Ad-hoc quote email, accounting improvements, AI lookup fix, and misc service updates
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file; QuotesController passes overrideEmail through to NotificationService - Quotes/Details view: SMS consent display, email/SMS send button state based on consent - Accounting module: AccountingDisplayHelpers for consistent ledger formatting; AccountsController + Accounts views improvements; AccountingEnums additions - Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController - InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path (LookupByUrlAsync already has it built in — was double-fetching) - PdfService: quote/invoice PDF updates - PricingCalculationService: minor pricing logic fix - QuoteProfile: mapping updates for new quote fields - ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -154,7 +154,7 @@
|
||||
(function () {
|
||||
// SubType enum values → AccountType enum values (mirrors server-side mapping)
|
||||
const subTypeToAccountType = {
|
||||
1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Assets
|
||||
8: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Assets
|
||||
10: 2, 11: 2, 12: 2, 13: 2, // Liabilities
|
||||
20: 3, 21: 3, // Equity
|
||||
30: 4, 31: 4, 32: 4, // Revenue
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<script>
|
||||
// Auto-set AccountType when SubType is changed
|
||||
const subTypeToAccountType = {
|
||||
1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Asset
|
||||
8: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, // Asset
|
||||
10: 2, 11: 2, 12: 2, 13: 2, // Liability
|
||||
20: 3, 21: 3, // Equity
|
||||
30: 4, 31: 4, 32: 4, // Revenue
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<span class="badge bg-secondary ms-1" title="System account — cannot be deleted">sys</span>
|
||||
}
|
||||
</td>
|
||||
<td><span class="text-muted small">@acct.AccountSubType</span></td>
|
||||
<td><span class="text-muted small">@acct.AccountSubType.ToDisplayName()</span></td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(acct.ParentAccountName))
|
||||
{
|
||||
|
||||
@@ -29,10 +29,11 @@
|
||||
_ => "bi-journal"
|
||||
};
|
||||
|
||||
string typeLabel = Model.AccountType == AccountType.CostOfGoods ? "Cost of Goods Sold" : Model.AccountType.ToString();
|
||||
string typeLabel = Model.AccountType.ToDisplayName();
|
||||
|
||||
// Derive from AccountSubType (more reliable than AccountType which users can misconfigure)
|
||||
bool normalDebitBalance =
|
||||
Model.AccountSubType == AccountSubType.Cash ||
|
||||
Model.AccountSubType == AccountSubType.Checking ||
|
||||
Model.AccountSubType == AccountSubType.Savings ||
|
||||
Model.AccountSubType == AccountSubType.AccountsReceivable ||
|
||||
@@ -71,7 +72,7 @@
|
||||
<div>
|
||||
<p class="text-muted mb-0">
|
||||
<span class="badge bg-@typeColor bg-opacity-75 me-1">@typeLabel</span>
|
||||
<span class="text-muted small">@Model.AccountSubType · @balanceLabel</span>
|
||||
<span class="text-muted small">@Model.AccountSubType.ToDisplayName() · @balanceLabel</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
|
||||
@@ -92,6 +92,21 @@
|
||||
<p><strong>Contact Name:</strong> @(Model.ProspectContactName ?? "-")</p>
|
||||
<p><strong>Email:</strong> @(Model.ProspectEmail ?? "-")</p>
|
||||
<p><strong>Phone:</strong> @(Model.ProspectPhone ?? "-")</p>
|
||||
<p>
|
||||
<strong>SMS Consent:</strong>
|
||||
@if (Model.ProspectSmsConsent)
|
||||
{
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Consented</span>
|
||||
@if (Model.ProspectSmsConsentedAt.HasValue)
|
||||
{
|
||||
<span class="text-muted small ms-1">on @Model.ProspectSmsConsentedAt.Value.ToLocalTime().ToString("MM/dd/yyyy")</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted small"><i class="bi bi-dash-circle me-1"></i>Not recorded</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Address:</strong> @(Model.ProspectAddress ?? "-")</p>
|
||||
@@ -1528,6 +1543,8 @@
|
||||
var detHasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail);
|
||||
var detHasMobile = !string.IsNullOrWhiteSpace(Model.CustomerMobilePhone);
|
||||
var detHasSmsConsent = Model.CustomerNotifyBySms && detHasMobile;
|
||||
var detProspectHasPhone = Model.IsProspect && !string.IsNullOrWhiteSpace(Model.ProspectPhone);
|
||||
var detProspectSmsReady = detProspectHasPhone && Model.ProspectSmsConsent;
|
||||
}
|
||||
@if (Model.StatusCode != "APPROVED" && Model.StatusCode != "CONVERTED")
|
||||
{
|
||||
@@ -1549,19 +1566,36 @@
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
@if (detHasEmail)
|
||||
@{
|
||||
var detEmailOptedOut = detHasEmail && !Model.CustomerNotifyByEmail;
|
||||
}
|
||||
@if (detEmailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary" disabled
|
||||
title="@Model.CustomerName has email notifications turned off">
|
||||
<i class="bi bi-envelope-arrow-up me-1"></i>Send Quote via Email
|
||||
</button>
|
||||
}
|
||||
else if (detHasEmail || !string.IsNullOrWhiteSpace(Model.ProspectEmail))
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary" onclick="resendQuote(@Model.Id)">
|
||||
<i class="bi bi-envelope-arrow-up me-1"></i>Send Quote via Email
|
||||
</button>
|
||||
}
|
||||
@if (detHasMobile)
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
data-bs-toggle="modal" data-bs-target="#quoteAdHocEmailModal">
|
||||
<i class="bi bi-envelope-arrow-up me-1"></i>Send Quote via Email
|
||||
</button>
|
||||
}
|
||||
@if (detHasMobile || detProspectSmsReady)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-info" onclick="sendQuoteSms(@Model.Id)">
|
||||
<i class="bi bi-chat-dots me-1"></i>Send Quote via SMS
|
||||
</button>
|
||||
}
|
||||
@if (!detHasMobile && !detHasEmail)
|
||||
@if (!detHasMobile && !detHasEmail && !detProspectHasPhone && string.IsNullOrWhiteSpace(Model.ProspectEmail))
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent py-1 px-2 small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>No email or mobile number on file — update the customer record to send this quote electronically.
|
||||
@@ -1571,6 +1605,10 @@
|
||||
{
|
||||
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent required to send via text.</div>
|
||||
}
|
||||
@if (detProspectHasPhone && !Model.ProspectSmsConsent)
|
||||
{
|
||||
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent not recorded — edit the quote to enable SMS for this prospect.</div>
|
||||
}
|
||||
@if (!Model.ConvertedToJobId.HasValue)
|
||||
{
|
||||
<form asp-action="ConvertToJob" asp-route-id="@Model.Id" method="post" class="d-inline" id="createJobForm">
|
||||
@@ -2103,6 +2141,30 @@
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- Ad-hoc Email Modal (no email on file) -->
|
||||
<div class="modal fade" id="quoteAdHocEmailModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-envelope-arrow-up me-2"></i>Send Quote via Email</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted small mb-3">No email address is on file for this customer. Enter an address to send to:</p>
|
||||
<label for="quoteAdHocEmailInput" class="form-label fw-medium">Send To</label>
|
||||
<input type="email" id="quoteAdHocEmailInput" class="form-control" placeholder="recipient@example.com" />
|
||||
<div id="quoteAdHocEmailError" class="text-danger small mt-1 d-none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="sendQuoteToAdHocEmail(@Model.Id)">
|
||||
<i class="bi bi-send me-1"></i>Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Quote via SMS Modal -->
|
||||
<div class="modal fade" id="sendQuoteSmsModal" tabindex="-1" aria-labelledby="sendQuoteSmsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
@@ -2201,7 +2263,20 @@
|
||||
@section Scripts {
|
||||
<script src="~/js/customer-change.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
function resendQuote(quoteId) {
|
||||
function sendQuoteToAdHocEmail(quoteId) {
|
||||
const email = (document.getElementById('quoteAdHocEmailInput').value ?? '').trim();
|
||||
const errDiv = document.getElementById('quoteAdHocEmailError');
|
||||
if (!email || !email.includes('@@')) {
|
||||
errDiv.textContent = 'Please enter a valid email address.';
|
||||
errDiv.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
errDiv.classList.add('d-none');
|
||||
bootstrap.Modal.getInstance(document.getElementById('quoteAdHocEmailModal'))?.hide();
|
||||
resendQuote(quoteId, email);
|
||||
}
|
||||
|
||||
function resendQuote(quoteId, overrideEmail) {
|
||||
// Reset modal state
|
||||
document.getElementById('sendQuoteSending').classList.remove('d-none');
|
||||
document.getElementById('sendQuoteResult').classList.add('d-none');
|
||||
@@ -2212,8 +2287,10 @@
|
||||
modal.show();
|
||||
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
const url = '@Url.Action("ResendQuote", "Quotes")?id=' + quoteId
|
||||
+ (overrideEmail ? '&overrideEmail=' + encodeURIComponent(overrideEmail) : '');
|
||||
|
||||
fetch('@Url.Action("ResendQuote", "Quotes")?id=' + quoteId, {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': token, 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user