diff --git a/src/PowderCoating.Infrastructure/Services/NotificationService.cs b/src/PowderCoating.Infrastructure/Services/NotificationService.cs index 3c09bc7..747a32e 100644 --- a/src/PowderCoating.Infrastructure/Services/NotificationService.cs +++ b/src/PowderCoating.Infrastructure/Services/NotificationService.cs @@ -94,7 +94,7 @@ public class NotificationService : INotificationService quote.CompanyId, NotificationType.QuoteSent, values, $"Your Quote {quote.QuoteNumber} from {companyName}"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, baseUrl); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, baseUrl, replyToEmail); var plainText = StripHtml(fullHtml); var (success, error) = await _emailService.SendEmailAsync( @@ -137,7 +137,7 @@ public class NotificationService : INotificationService quote.CompanyId, NotificationType.QuoteSent, values, $"Your Quote {quote.QuoteNumber} from {companyName}"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, baseUrl); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, baseUrl, replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -300,7 +300,7 @@ public class NotificationService : INotificationService quote.CompanyId, NotificationType.QuoteApproved, values, $"Quote {quote.QuoteNumber} Approved — {companyName}"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -383,7 +383,7 @@ public class NotificationService : INotificationService var (subject, htmlBody) = await GetRenderedEmailAsync( job.CompanyId, notifType, values, defaultSubject); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -451,7 +451,7 @@ public class NotificationService : INotificationService job.CompanyId, NotificationType.JobCompleted, values, $"Job {job.JobNumber} Complete — {companyName}"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -674,7 +674,7 @@ public class NotificationService : INotificationService """; } - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = !string.IsNullOrEmpty(paymentUrl) ? StripHtml(htmlBody) + $"\r\n\r\nPay online: {paymentUrl}" : StripHtml(fullHtml); @@ -793,7 +793,7 @@ public class NotificationService : INotificationService invoice.CompanyId, NotificationType.PaymentReceived, values, $"Payment Received — Invoice {invoice.InvoiceNumber}"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -867,7 +867,7 @@ public class NotificationService : INotificationService invoice.CompanyId, NotificationType.PaymentReminder, values, $"Payment Reminder — Invoice {invoice.InvoiceNumber} ({daysOverdue} days overdue)"); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error, recipientsLog) = await SendToEmailListAsync( @@ -971,7 +971,7 @@ public class NotificationService : INotificationService var (subject, htmlBody) = await GetRenderedEmailAsync( quote.CompanyId, notificationType, values, defaultSubject); - var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, await GetBaseUrlAsync()); + var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, await GetBaseUrlAsync(), replyToEmail); var plainText = StripHtml(fullHtml); var (success, error) = await _emailService.SendEmailAsync( @@ -1218,7 +1218,7 @@ public class NotificationService : INotificationService var (custSubject, custHtml) = await GetRenderedEmailAsync( appointment.CompanyId, NotificationType.AppointmentReminder, customerValues, defaultSubject); - var custFullHtml = AppendUnsubscribeFooterHtml(custHtml, customer.UnsubscribeToken, company, baseUrl); + var custFullHtml = AppendUnsubscribeFooterHtml(custHtml, customer.UnsubscribeToken, company, baseUrl, replyToEmail); var custPlainText = StripHtml(custFullHtml); var (custOk, custErr, custLog) = await SendToEmailListAsync( @@ -1388,17 +1388,25 @@ public class NotificationService : INotificationService /// /// Appends CAN-SPAM required footer as HTML. /// - private static string AppendUnsubscribeFooterHtml(string htmlBody, string? token, Company? company = null, string? baseUrl = null) + private static string AppendUnsubscribeFooterHtml(string htmlBody, string? token, Company? company = null, string? baseUrl = null, string? replyToEmail = null) { var hasUnsubscribeUrl = !string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(baseUrl); - var hasAddress = company != null && !string.IsNullOrWhiteSpace(company.Address); + var hasAddress = company != null && !string.IsNullOrWhiteSpace(company.Address); + var hasReplyTo = !string.IsNullOrWhiteSpace(replyToEmail); - if (!hasUnsubscribeUrl && !hasAddress) + if (!hasUnsubscribeUrl && !hasAddress && !hasReplyTo) return htmlBody; var footer = "
" + "

"; + if (hasReplyTo) + { + var encodedEmail = WebUtility.HtmlEncode(replyToEmail!); + footer += $"Questions? Reply to this email or contact us at {encodedEmail}"; + if (hasAddress || hasUnsubscribeUrl) footer += "
"; + } + if (hasAddress) { var addressLine = BuildAddressLine(company!); diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index 7929231..ff3cd3d 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -1957,12 +1957,10 @@ public class QuotesController : Controller if (dto.SmsConsent) await _notificationService.NotifySmsConsentGrantedAsync(customer); - // Get "Converted" status (cached) - var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; - var statuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyId); - var convertedStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Converted); - - // Update quote to link to new customer + // Update quote to link to new customer. + // Do NOT set "Converted" status here — that status is reserved for when a job is + // actually created via CreateJobFromQuote. Keeping the quote at "Approved" lets the + // user immediately click "Create Job from Quote" on the next screen. quote.CustomerId = customer.Id; // Clear prospect fields @@ -1977,14 +1975,11 @@ public class QuotesController : Controller quote.ProspectSmsConsent = false; quote.ProspectSmsConsentedAt = null; - // Update status to converted - quote.QuoteStatusId = convertedStatus?.Id ?? quote.QuoteStatusId; - await _unitOfWork.Quotes.UpdateAsync(quote); await _unitOfWork.CompleteAsync(); - this.ToastSuccess($"Prospect/Walk-In successfully converted to customer! Quote {quote.QuoteNumber} has been updated."); - return RedirectToAction("Details", "Customers", new { id = customer.Id }); + this.ToastSuccess($"Customer record created! You can now create a job from quote {quote.QuoteNumber}."); + return RedirectToAction(nameof(Details), new { id = dto.QuoteId }); } catch (Exception ex) {