Add AI Quick Quote widget and inline customer reassignment
- New AI Quick Quote floating button: staff type a verbal description to get an instant price estimate for phone/walk-in customers; detected color names are fuzzy-matched against inventory for stock status; saves draft quote under a Walk-In / Phone customer with one click - Inline customer change on Quote Details and Job Details: always-visible native select with inline confirmation banner (no TomSelect dependency); ChangeCustomer AJAX endpoints on QuotesController and JobsController - Quote Edit page: customer dropdown is now editable (lock removed) - Fix AutoMapper missing CatalogCategory -> UpdateCategoryDto mapping that caused a crash on the catalog category Edit page - Help docs and AI knowledge base updated for all three features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -466,6 +466,20 @@ public class QuotesController : Controller
|
||||
.ToListAsync();
|
||||
ViewBag.Deposits = quoteDeposits;
|
||||
|
||||
// Customer list for inline customer-change dropdown
|
||||
var allCustomers = await _unitOfWork.Customers.GetAllAsync();
|
||||
ViewBag.CustomerSelectList = allCustomers
|
||||
.Where(c => c.IsActive)
|
||||
.Select(c => new SelectListItem
|
||||
{
|
||||
Value = c.Id.ToString(),
|
||||
Text = !string.IsNullOrWhiteSpace(c.CompanyName)
|
||||
? c.CompanyName
|
||||
: $"{c.ContactFirstName} {c.ContactLastName}".Trim()
|
||||
})
|
||||
.OrderBy(c => c.Text)
|
||||
.ToList();
|
||||
|
||||
return View(quoteDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -476,6 +490,40 @@ public class QuotesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reassigns a quote to a different customer. Clears any prospect fields so the
|
||||
/// quote is treated as a real-customer quote after reassignment.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ChangeCustomer(int id, int customerId)
|
||||
{
|
||||
var quote = await _unitOfWork.Quotes.GetByIdAsync(id);
|
||||
if (quote == null) return NotFound();
|
||||
|
||||
var customer = await _unitOfWork.Customers.GetByIdAsync(customerId);
|
||||
if (customer == null)
|
||||
return Json(new { success = false, error = "Customer not found." });
|
||||
|
||||
quote.CustomerId = customerId;
|
||||
quote.ProspectCompanyName = null;
|
||||
quote.ProspectContactName = null;
|
||||
quote.ProspectEmail = null;
|
||||
quote.ProspectPhone = null;
|
||||
quote.ProspectAddress = null;
|
||||
quote.ProspectCity = null;
|
||||
quote.ProspectState = null;
|
||||
quote.ProspectZipCode = null;
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
var customerName = !string.IsNullOrWhiteSpace(customer.CompanyName)
|
||||
? customer.CompanyName
|
||||
: $"{customer.ContactFirstName} {customer.ContactLastName}".Trim();
|
||||
|
||||
return Json(new { success = true, customerName, customerId = customer.Id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates and streams the quote PDF.
|
||||
/// When <paramref name="inline"/> is true the browser displays it in a viewer tab;
|
||||
@@ -1299,13 +1347,8 @@ public class QuotesController : Controller
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Loaded quote {QuoteNumber}, Original CustomerId: {CustomerId}", quote.QuoteNumber, quote.CustomerId);
|
||||
|
||||
// Preserve original customer/prospect assignment (cannot be changed after creation)
|
||||
dto.CustomerId = quote.CustomerId;
|
||||
dto.IsForProspect = !quote.CustomerId.HasValue;
|
||||
|
||||
_logger.LogInformation("After preservation - CustomerId: {CustomerId}, IsForProspect: {IsForProspect}", dto.CustomerId, dto.IsForProspect);
|
||||
// IsForProspect derives from whether a customer was selected in the form
|
||||
dto.IsForProspect = !dto.CustomerId.HasValue;
|
||||
|
||||
// Validate at least one quote item exists
|
||||
if (dto.QuoteItems == null || dto.QuoteItems.Count == 0)
|
||||
|
||||
Reference in New Issue
Block a user