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:
2026-06-10 12:46:08 -04:00
parent 711cd01cd3
commit 94a89ee175
22 changed files with 12586 additions and 31 deletions
@@ -216,27 +216,50 @@
</h5>
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.Address))
@{
bool hasBilling = !string.IsNullOrEmpty(Model.Address);
bool hasShipTo = !string.IsNullOrEmpty(Model.ShipToAddress) || !string.IsNullOrEmpty(Model.ShipToCity);
}
@if (hasShipTo)
{
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Billing Address</label>
@if (hasBilling)
{
<p class="mb-1">@Model.Address</p>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.City)) { <span>@Model.City</span> }
@if (!string.IsNullOrEmpty(Model.State)) { <span>, @Model.State</span> }
@if (!string.IsNullOrEmpty(Model.ZipCode)) { <span> @Model.ZipCode</span> }
</p>
@if (!string.IsNullOrEmpty(Model.Country)) { <p class="mb-0 text-muted">@Model.Country</p> }
}
else { <p class="text-muted mb-0">Not provided</p> }
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">
<i class="bi bi-truck me-1"></i>Ship-To / Pickup Address
</label>
<p class="mb-1">@Model.ShipToAddress</p>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.ShipToCity)) { <span>@Model.ShipToCity</span> }
@if (!string.IsNullOrEmpty(Model.ShipToState)) { <span>, @Model.ShipToState</span> }
@if (!string.IsNullOrEmpty(Model.ShipToZipCode)) { <span> @Model.ShipToZipCode</span> }
</p>
@if (!string.IsNullOrEmpty(Model.ShipToCountry)) { <p class="mb-0 text-muted">@Model.ShipToCountry</p> }
</div>
</div>
}
else if (hasBilling)
{
<p class="mb-2">@Model.Address</p>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.City))
{
<span>@Model.City</span>
}
@if (!string.IsNullOrEmpty(Model.State))
{
<span>, @Model.State</span>
}
@if (!string.IsNullOrEmpty(Model.ZipCode))
{
<span> @Model.ZipCode</span>
}
@if (!string.IsNullOrEmpty(Model.City)) { <span>@Model.City</span> }
@if (!string.IsNullOrEmpty(Model.State)) { <span>, @Model.State</span> }
@if (!string.IsNullOrEmpty(Model.ZipCode)) { <span> @Model.ZipCode</span> }
</p>
@if (!string.IsNullOrEmpty(Model.Country))
{
<p class="mb-0 text-muted">@Model.Country</p>
}
@if (!string.IsNullOrEmpty(Model.Country)) { <p class="mb-0 text-muted">@Model.Country</p> }
}
else
{
@@ -262,6 +285,15 @@
<label class="text-muted small mb-1">Payment Terms</label>
<p class="mb-0">@(Model.PaymentTerms ?? "Not set")</p>
</div>
@if (!string.IsNullOrEmpty(Model.LeadSource))
{
<div class="col-md-6">
<label class="text-muted small mb-1">Lead Source</label>
<p class="mb-0">
<i class="bi bi-signpost me-1 text-muted"></i>@Model.LeadSource
</p>
</div>
}
<div class="col-md-6">
<label class="text-muted small mb-1">Credit Limit</label>
<p class="mb-0 fw-semibold">@Model.CreditLimit.ToString("C")</p>
@@ -329,6 +361,96 @@
</div>
}
<!-- Additional Contacts -->
@{
var customerContacts = ViewBag.CustomerContacts as List<PowderCoating.Core.Entities.CustomerContact>;
}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-people me-2 text-primary"></i>Additional Contacts
</h5>
<div class="d-flex align-items-center gap-2">
<span class="text-muted small">
<i class="bi bi-info-circle me-1"></i>For staff reference &mdash; automated notifications still go to the primary contact above.
</span>
<button type="button" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal" data-bs-target="#contactModal"
onclick="openAddContactModal()">
<i class="bi bi-plus-circle me-1"></i>Add Contact
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Name / Role</th>
<th>Email</th>
<th>Phone</th>
<th class="text-end pe-3"></th>
</tr>
</thead>
<tbody id="contacts-table-body">
@if (customerContacts != null && customerContacts.Count > 0)
{
@foreach (var c in customerContacts)
{
var displayName = string.IsNullOrWhiteSpace(c.LastName) ? c.FirstName : $"{c.FirstName} {c.LastName}";
<tr data-contact-id="@c.Id">
<td class="ps-3">
<div class="fw-semibold">
@displayName
@if (!string.IsNullOrEmpty(c.ContactRole))
{
<span class="badge bg-secondary bg-opacity-10 text-secondary ms-1">@c.ContactRole</span>
}
</div>
@if (!string.IsNullOrEmpty(c.Title))
{
<div class="text-muted" style="font-size:0.75rem;">@c.Title</div>
}
</td>
<td>
@if (!string.IsNullOrEmpty(c.Email))
{
<a href="mailto:@c.Email" class="text-decoration-none small">@c.Email</a>
}
else { <span class="text-muted small">&mdash;</span> }
</td>
<td>
@if (!string.IsNullOrEmpty(c.Phone ?? c.MobilePhone))
{
<span class="small">@(c.Phone ?? c.MobilePhone)</span>
}
else { <span class="text-muted small">&mdash;</span> }
</td>
<td class="text-end pe-3">
<button type="button" class="btn btn-sm btn-outline-secondary me-1"
onclick="editContact(@Model.Id, @c.Id)" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="deleteContact(@Model.Id, @c.Id)" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
}
else
{
<tr id="no-contacts-placeholder">
<td colspan="4" class="text-muted small px-3 py-2">No additional contacts. Click &ldquo;Add Contact&rdquo; to add billing, ops, or drop-off contacts.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Customer Notes -->
@{
var customerNotes = ViewBag.CustomerNotes as List<PowderCoating.Core.Entities.CustomerNote>;
@@ -776,6 +898,72 @@
</div>
</div>
<!-- Add / Edit Contact Modal -->
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="contactModalLabel">
<i class="bi bi-person-plus me-2 text-primary"></i><span id="contactModalTitle">Add Contact</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="contactId" value="0" />
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">First Name <span class="text-danger">*</span></label>
<input type="text" id="contactFirstName" class="form-control" maxlength="100" placeholder="First name" />
</div>
<div class="col-md-6">
<label class="form-label">Last Name</label>
<input type="text" id="contactLastName" class="form-control" maxlength="100" placeholder="Last name" />
</div>
<div class="col-md-6">
<label class="form-label">Job Title</label>
<input type="text" id="contactTitle" class="form-control" maxlength="100" placeholder="e.g. Purchasing Manager" />
</div>
<div class="col-md-6">
<label class="form-label">Role</label>
<select id="contactRole" class="form-select">
<option value="">&mdash; Select &mdash;</option>
<option value="Billing">Billing</option>
<option value="Operations">Operations</option>
<option value="Drop-Off">Drop-Off</option>
<option value="Sales">Sales</option>
<option value="General">General</option>
<option value="Other">Other</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Email</label>
<input type="email" id="contactEmail" class="form-control" maxlength="200" placeholder="email@example.com" />
</div>
<div class="col-md-6">
<label class="form-label">Phone</label>
<input type="tel" id="contactPhone" class="form-control" maxlength="20" placeholder="(555) 123-4567" />
</div>
<div class="col-md-6">
<label class="form-label">Mobile Phone</label>
<input type="tel" id="contactMobilePhone" class="form-control" maxlength="20" placeholder="(555) 123-4567" />
</div>
<div class="col-12">
<label class="form-label">Notes</label>
<textarea id="contactNotes" class="form-control" rows="2" maxlength="500" placeholder="Optional notes about this contact..."></textarea>
</div>
</div>
<div id="contactModalError" class="alert alert-danger alert-permanent mt-3 d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveContact(@Model.Id)" id="saveContactBtn">
<i class="bi bi-check-circle me-1"></i>Save Contact
</button>
</div>
</div>
</div>
</div>
<!-- Add Store Credit Modal -->
@if (User.IsInRole("SuperAdmin") || User.IsInRole("Administrator"))
{