Add CRM features: Additional Contacts, Lead Source, Ship-To Address; update Help docs
- New CustomerContact entity + migration (AddCustomerContactsAndCrmFields) - Customer.LeadSource + ShipToAddress/City/State/ZipCode/Country fields - Additional Contacts card on Customer Details with AJAX add/edit/delete - Lead Source dropdown on Create/Edit; Ship-To section on Create/Edit - Customer Details: side-by-side billing/ship-to when ship-to is set - Help docs: Customers (contacts, ship-to, lead source, preferred powders, outstanding pickups) - Help docs: Jobs (clone job, project name), Quotes (project name), Invoices (project name), Inventory (low stock clickable filter) - HelpKnowledgeBase.cs updated for all features above Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,132 @@ async function removePreferredPowder(customerId, itemId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Customer Contacts ──────────────────────────────────────────────────────
|
||||
|
||||
function openAddContactModal() {
|
||||
document.getElementById('contactId').value = '0';
|
||||
document.getElementById('contactModalTitle').textContent = 'Add Contact';
|
||||
document.getElementById('contactFirstName').value = '';
|
||||
document.getElementById('contactLastName').value = '';
|
||||
document.getElementById('contactTitle').value = '';
|
||||
document.getElementById('contactRole').value = '';
|
||||
document.getElementById('contactEmail').value = '';
|
||||
document.getElementById('contactPhone').value = '';
|
||||
document.getElementById('contactMobilePhone').value = '';
|
||||
document.getElementById('contactNotes').value = '';
|
||||
document.getElementById('contactModalError').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function editContact(customerId, contactId) {
|
||||
const tok = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
try {
|
||||
const res = await fetch(`/Customers/GetContact/${customerId}?contactId=${contactId}`);
|
||||
const data = await res.json();
|
||||
if (!data.success) { toastr.error('Could not load contact.'); return; }
|
||||
|
||||
const c = data.contact;
|
||||
document.getElementById('contactId').value = c.id;
|
||||
document.getElementById('contactModalTitle').textContent = 'Edit Contact';
|
||||
document.getElementById('contactFirstName').value = c.firstName ?? '';
|
||||
document.getElementById('contactLastName').value = c.lastName ?? '';
|
||||
document.getElementById('contactTitle').value = c.title ?? '';
|
||||
document.getElementById('contactRole').value = c.contactRole ?? '';
|
||||
document.getElementById('contactEmail').value = c.email ?? '';
|
||||
document.getElementById('contactPhone').value = c.phone ?? '';
|
||||
document.getElementById('contactMobilePhone').value = c.mobilePhone ?? '';
|
||||
document.getElementById('contactNotes').value = c.notes ?? '';
|
||||
document.getElementById('contactModalError').classList.add('d-none');
|
||||
|
||||
new bootstrap.Modal(document.getElementById('contactModal')).show();
|
||||
} catch {
|
||||
toastr.error('An error occurred loading the contact.');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveContact(customerId) {
|
||||
const tok = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
const contactId = parseInt(document.getElementById('contactId').value ?? '0', 10);
|
||||
const firstName = document.getElementById('contactFirstName').value.trim();
|
||||
|
||||
if (!firstName) {
|
||||
const err = document.getElementById('contactModalError');
|
||||
err.textContent = 'First name is required.';
|
||||
err.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
FirstName: firstName,
|
||||
LastName: document.getElementById('contactLastName').value.trim(),
|
||||
Title: document.getElementById('contactTitle').value.trim(),
|
||||
ContactRole: document.getElementById('contactRole').value,
|
||||
Email: document.getElementById('contactEmail').value.trim(),
|
||||
Phone: document.getElementById('contactPhone').value.trim(),
|
||||
MobilePhone: document.getElementById('contactMobilePhone').value.trim(),
|
||||
Notes: document.getElementById('contactNotes').value.trim(),
|
||||
});
|
||||
|
||||
const isEdit = contactId > 0;
|
||||
if (isEdit) { params.append('Id', contactId); params.append('CustomerId', customerId); }
|
||||
|
||||
const url = isEdit ? `/Customers/UpdateContact/${customerId}` : `/Customers/AddContact/${customerId}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': tok },
|
||||
body: params.toString()
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('contactModal'))?.hide();
|
||||
const tbody = document.getElementById('contacts-table-body');
|
||||
const placeholder = document.getElementById('no-contacts-placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
if (isEdit) {
|
||||
const existing = tbody.querySelector(`tr[data-contact-id="${contactId}"]`);
|
||||
if (existing) existing.outerHTML = data.rowHtml;
|
||||
else tbody.insertAdjacentHTML('beforeend', data.rowHtml);
|
||||
} else {
|
||||
tbody.insertAdjacentHTML('beforeend', data.rowHtml);
|
||||
}
|
||||
toastr.success(isEdit ? 'Contact updated.' : 'Contact added.');
|
||||
} else {
|
||||
const err = document.getElementById('contactModalError');
|
||||
err.textContent = data.message || 'An error occurred.';
|
||||
err.classList.remove('d-none');
|
||||
}
|
||||
} catch {
|
||||
toastr.error('An error occurred. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteContact(customerId, contactId) {
|
||||
if (!confirm('Delete this contact?')) return;
|
||||
const tok = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
try {
|
||||
const res = await fetch(`/Customers/DeleteContact/${customerId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'RequestVerificationToken': tok },
|
||||
body: `contactId=${contactId}`
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.querySelector(`tr[data-contact-id="${contactId}"]`)?.remove();
|
||||
const tbody = document.getElementById('contacts-table-body');
|
||||
if (tbody && tbody.querySelectorAll('tr[data-contact-id]').length === 0)
|
||||
tbody.insertAdjacentHTML('afterbegin', '<tr id="no-contacts-placeholder"><td colspan="4" class="text-muted small px-3 py-2">No additional contacts.</td></tr>');
|
||||
toastr.success('Contact deleted.');
|
||||
} else {
|
||||
toastr.error(data.message || 'Could not delete contact.');
|
||||
}
|
||||
} catch {
|
||||
toastr.error('An error occurred. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
window.updateCustomerSmsStatus = function () {
|
||||
const section = document.getElementById('sms-status-section');
|
||||
if (!section) return;
|
||||
|
||||
Reference in New Issue
Block a user