Initial commit
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
@using PowderCoating.Application.DTOs.Job
|
||||
@using PowderCoating.Application.DTOs.Quote
|
||||
@using PowderCoating.Application.DTOs.Common
|
||||
|
||||
@{
|
||||
ViewData["Title"] = $"Activity - {ViewBag.CustomerName}";
|
||||
ViewData["PageIcon"] = "bi-clock-history";
|
||||
|
||||
var customerId = (int)ViewBag.CustomerId;
|
||||
var customerName = (string)ViewBag.CustomerName;
|
||||
var activeTab = (string)ViewBag.ActiveTab;
|
||||
|
||||
var jobs = (PagedResult<JobListDto>)ViewBag.Jobs;
|
||||
var jobSort = (string)ViewBag.JobSort;
|
||||
var jobDir = (string)ViewBag.JobDir;
|
||||
|
||||
var quotes = (PagedResult<QuoteListDto>)ViewBag.Quotes;
|
||||
var quoteSort = (string)ViewBag.QuoteSort;
|
||||
var quoteDir = (string)ViewBag.QuoteDir;
|
||||
|
||||
// Helper: build a URL that changes only the job sort column/direction
|
||||
string JobSortUrl(string col)
|
||||
{
|
||||
var newDir = jobSort == col && jobDir == "asc" ? "desc" : "asc";
|
||||
return Url.Action("Activity", new
|
||||
{
|
||||
id = customerId,
|
||||
activeTab = "jobs",
|
||||
jobSort = col,
|
||||
jobDir = newDir,
|
||||
jobPage = 1,
|
||||
jobSize = jobs.PageSize,
|
||||
quoteSort,
|
||||
quoteDir,
|
||||
quotePage = quotes.PageNumber,
|
||||
quoteSize = quotes.PageSize
|
||||
})!;
|
||||
}
|
||||
|
||||
string JobSortIcon(string col)
|
||||
{
|
||||
if (jobSort != col) return "bi-arrow-down-up";
|
||||
return jobDir == "asc" ? "bi-arrow-up" : "bi-arrow-down";
|
||||
}
|
||||
|
||||
// Helper: build a URL that changes only the quote sort column/direction
|
||||
string QuoteSortUrl(string col)
|
||||
{
|
||||
var newDir = quoteSort == col && quoteDir == "asc" ? "desc" : "asc";
|
||||
return Url.Action("Activity", new
|
||||
{
|
||||
id = customerId,
|
||||
activeTab = "quotes",
|
||||
jobSort,
|
||||
jobDir,
|
||||
jobPage = jobs.PageNumber,
|
||||
jobSize = jobs.PageSize,
|
||||
quoteSort = col,
|
||||
quoteDir = newDir,
|
||||
quotePage = 1,
|
||||
quoteSize = quotes.PageSize
|
||||
})!;
|
||||
}
|
||||
|
||||
string QuoteSortIcon(string col)
|
||||
{
|
||||
if (quoteSort != col) return "bi-arrow-down-up";
|
||||
return quoteDir == "asc" ? "bi-arrow-up" : "bi-arrow-down";
|
||||
}
|
||||
|
||||
// Helper: build page-change URL for jobs
|
||||
string JobPageUrl(int page)
|
||||
{
|
||||
return Url.Action("Activity", new
|
||||
{
|
||||
id = customerId,
|
||||
activeTab = "jobs",
|
||||
jobSort,
|
||||
jobDir,
|
||||
jobPage = page,
|
||||
jobSize = jobs.PageSize,
|
||||
quoteSort,
|
||||
quoteDir,
|
||||
quotePage = quotes.PageNumber,
|
||||
quoteSize = quotes.PageSize
|
||||
})!;
|
||||
}
|
||||
|
||||
// Helper: build page-change URL for quotes
|
||||
string QuotePageUrl(int page)
|
||||
{
|
||||
return Url.Action("Activity", new
|
||||
{
|
||||
id = customerId,
|
||||
activeTab = "quotes",
|
||||
jobSort,
|
||||
jobDir,
|
||||
jobPage = jobs.PageNumber,
|
||||
jobSize = jobs.PageSize,
|
||||
quoteSort,
|
||||
quoteDir,
|
||||
quotePage = page,
|
||||
quoteSize = quotes.PageSize
|
||||
})!;
|
||||
}
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Job
|
||||
</a>
|
||||
<a asp-controller="Quotes" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-outline-primary">
|
||||
<i class="bi bi-file-text me-2"></i>New Quote
|
||||
</a>
|
||||
<a asp-action="Details" asp-route-id="@customerId" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to Customer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Summary Badges -->
|
||||
<div class="d-flex gap-3 mb-4">
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary fs-6 px-3 py-2">
|
||||
<i class="bi bi-briefcase me-2"></i>@jobs.TotalCount Job@(jobs.TotalCount == 1 ? "" : "s")
|
||||
</span>
|
||||
<span class="badge bg-info bg-opacity-10 text-info fs-6 px-3 py-2">
|
||||
<i class="bi bi-file-text me-2"></i>@quotes.TotalCount Quote@(quotes.TotalCount == 1 ? "" : "s")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs mb-0" id="activityTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(activeTab == "jobs" ? "active" : "")" id="jobs-tab"
|
||||
data-bs-toggle="tab" data-bs-target="#jobs-panel"
|
||||
type="button" role="tab">
|
||||
<i class="bi bi-briefcase me-2"></i>Jobs
|
||||
<span class="badge bg-secondary ms-1">@jobs.TotalCount</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(activeTab == "quotes" ? "active" : "")" id="quotes-tab"
|
||||
data-bs-toggle="tab" data-bs-target="#quotes-panel"
|
||||
type="button" role="tab">
|
||||
<i class="bi bi-file-text me-2"></i>Quotes
|
||||
<span class="badge bg-secondary ms-1">@quotes.TotalCount</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="activityTabContent">
|
||||
|
||||
<!-- ===== JOBS TAB ===== -->
|
||||
<div class="tab-pane fade @(activeTab == "jobs" ? "show active" : "")" id="jobs-panel" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm border-top-0" style="border-top-left-radius:0; border-top-right-radius:0;">
|
||||
<div class="card-body p-0">
|
||||
@if (!jobs.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No jobs found</h5>
|
||||
<p class="text-muted mb-4">This customer has no jobs yet</p>
|
||||
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create First Job
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-4">
|
||||
<a href="@JobSortUrl("JobNumber")" class="text-decoration-none" style="color:inherit">
|
||||
Job Number <i class="bi @JobSortIcon("JobNumber")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Description</th>
|
||||
<th>
|
||||
<a href="@JobSortUrl("Status")" class="text-decoration-none" style="color:inherit">
|
||||
Status <i class="bi @JobSortIcon("Status")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@JobSortUrl("Priority")" class="text-decoration-none" style="color:inherit">
|
||||
Priority <i class="bi @JobSortIcon("Priority")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@JobSortUrl("DueDate")" class="text-decoration-none" style="color:inherit">
|
||||
Due Date <i class="bi @JobSortIcon("DueDate")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@JobSortUrl("FinalPrice")" class="text-decoration-none" style="color:inherit">
|
||||
Price <i class="bi @JobSortIcon("FinalPrice")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@JobSortUrl("CreatedAt")" class="text-decoration-none" style="color:inherit">
|
||||
Created <i class="bi @JobSortIcon("CreatedAt")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var job in jobs.Items)
|
||||
{
|
||||
<tr style="cursor:pointer;" onclick="window.location.href='@Url.Action("Details", "Jobs", new { id = job.Id })'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width:38px;height:38px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;">
|
||||
<i class="bi bi-briefcase"></i>
|
||||
</div>
|
||||
<span class="fw-semibold">@job.JobNumber</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-truncate" style="max-width:220px;">@job.Description</td>
|
||||
<td>
|
||||
<span class="badge bg-@job.StatusColorClass bg-opacity-10 text-@job.StatusColorClass">
|
||||
@job.StatusDisplayName
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-@job.PriorityColorClass">@job.PriorityDisplayName</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (job.DueDate.HasValue)
|
||||
{
|
||||
var overdue = job.DueDate.Value < DateTime.Now
|
||||
&& job.StatusCode != "COMPLETED"
|
||||
&& job.StatusCode != "DELIVERED";
|
||||
<span class="@(overdue ? "text-danger fw-semibold" : "")">
|
||||
@job.DueDate.Value.ToString("MMM dd, yyyy")
|
||||
@if (overdue) { <i class="bi bi-exclamation-triangle ms-1"></i> }
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="fw-semibold">@job.FinalPrice.ToString("C")</td>
|
||||
<td class="text-muted small">@job.CreatedAt.ToString("MMM dd, yyyy")</td>
|
||||
<td class="text-end pe-4" onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.Id"
|
||||
class="btn btn-outline-primary" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-controller="Jobs" asp-action="Edit" asp-route-id="@job.Id"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Pagination -->
|
||||
@if (jobs.TotalPages > 1)
|
||||
{
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
|
||||
<div class="text-muted small">
|
||||
Showing @jobs.StartIndex–@jobs.EndIndex of @jobs.TotalCount jobs
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="text-muted small me-1">Rows:</span>
|
||||
@foreach (var sz in new[] { 10, 25, 50 })
|
||||
{
|
||||
var szUrl = Url.Action("Activity", new
|
||||
{
|
||||
id = customerId, activeTab = "jobs",
|
||||
jobSort, jobDir, jobPage = 1, jobSize = sz,
|
||||
quoteSort, quoteDir, quotePage = quotes.PageNumber, quoteSize = quotes.PageSize
|
||||
});
|
||||
<a href="@szUrl" class="btn btn-sm @(jobs.PageSize == sz ? "btn-primary" : "btn-outline-secondary")">@sz</a>
|
||||
}
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item @(jobs.PageNumber == 1 ? "disabled" : "")">
|
||||
<a class="page-link" href="@JobPageUrl(jobs.PageNumber - 1)">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@for (var p = Math.Max(1, jobs.PageNumber - 2); p <= Math.Min(jobs.TotalPages, jobs.PageNumber + 2); p++)
|
||||
{
|
||||
<li class="page-item @(p == jobs.PageNumber ? "active" : "")">
|
||||
<a class="page-link" href="@JobPageUrl(p)">@p</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item @(jobs.PageNumber == jobs.TotalPages ? "disabled" : "")">
|
||||
<a class="page-link" href="@JobPageUrl(jobs.PageNumber + 1)">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== QUOTES TAB ===== -->
|
||||
<div class="tab-pane fade @(activeTab == "quotes" ? "show active" : "")" id="quotes-panel" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm border-top-0" style="border-top-left-radius:0; border-top-right-radius:0;">
|
||||
<div class="card-body p-0">
|
||||
@if (!quotes.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-file-text" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No quotes found</h5>
|
||||
<p class="text-muted mb-4">This customer has no quotes yet</p>
|
||||
<a asp-controller="Quotes" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create First Quote
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-4">
|
||||
<a href="@QuoteSortUrl("QuoteNumber")" class="text-decoration-none" style="color:inherit">
|
||||
Quote # <i class="bi @QuoteSortIcon("QuoteNumber")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@QuoteSortUrl("Status")" class="text-decoration-none" style="color:inherit">
|
||||
Status <i class="bi @QuoteSortIcon("Status")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@QuoteSortUrl("QuoteDate")" class="text-decoration-none" style="color:inherit">
|
||||
Quote Date <i class="bi @QuoteSortIcon("QuoteDate")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@QuoteSortUrl("Expiration")" class="text-decoration-none" style="color:inherit">
|
||||
Expires <i class="bi @QuoteSortIcon("Expiration")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@QuoteSortUrl("Total")" class="text-decoration-none" style="color:inherit">
|
||||
Total <i class="bi @QuoteSortIcon("Total")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var quote in quotes.Items)
|
||||
{
|
||||
<tr style="cursor:pointer;" onclick="window.location.href='@Url.Action("Details", "Quotes", new { id = quote.Id })'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width:38px;height:38px;background:linear-gradient(135deg,#11998e 0%,#38ef7d 100%);color:white;">
|
||||
<i class="bi bi-file-text"></i>
|
||||
</div>
|
||||
<span class="fw-semibold">@quote.QuoteNumber</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var isExpired = quote.IsExpired;
|
||||
}
|
||||
@if (isExpired)
|
||||
{
|
||||
<span class="badge bg-danger bg-opacity-10 text-danger">Expired</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-@quote.StatusColorClass bg-opacity-10 text-@quote.StatusColorClass">
|
||||
@quote.StatusDisplayName
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>@quote.QuoteDate.ToString("MMM dd, yyyy")</td>
|
||||
<td>
|
||||
@if (quote.ExpirationDate.HasValue)
|
||||
{
|
||||
<span class="@(isExpired ? "text-danger fw-semibold" : "")">
|
||||
@quote.ExpirationDate.Value.ToString("MMM dd, yyyy")
|
||||
@if (isExpired) { <i class="bi bi-exclamation-triangle ms-1"></i> }
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="fw-semibold">@quote.Total.ToString("C")</td>
|
||||
<td class="text-end pe-4" onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-controller="Quotes" asp-action="Details" asp-route-id="@quote.Id"
|
||||
class="btn btn-outline-primary" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-controller="Quotes" asp-action="Edit" asp-route-id="@quote.Id"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Quotes Pagination -->
|
||||
@if (quotes.TotalPages > 1)
|
||||
{
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
|
||||
<div class="text-muted small">
|
||||
Showing @quotes.StartIndex–@quotes.EndIndex of @quotes.TotalCount quotes
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="text-muted small me-1">Rows:</span>
|
||||
@foreach (var sz in new[] { 10, 25, 50 })
|
||||
{
|
||||
var szUrl = Url.Action("Activity", new
|
||||
{
|
||||
id = customerId, activeTab = "quotes",
|
||||
jobSort, jobDir, jobPage = jobs.PageNumber, jobSize = jobs.PageSize,
|
||||
quoteSort, quoteDir, quotePage = 1, quoteSize = sz
|
||||
});
|
||||
<a href="@szUrl" class="btn btn-sm @(quotes.PageSize == sz ? "btn-primary" : "btn-outline-secondary")">@sz</a>
|
||||
}
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item @(quotes.PageNumber == 1 ? "disabled" : "")">
|
||||
<a class="page-link" href="@QuotePageUrl(quotes.PageNumber - 1)">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@for (var p = Math.Max(1, quotes.PageNumber - 2); p <= Math.Min(quotes.TotalPages, quotes.PageNumber + 2); p++)
|
||||
{
|
||||
<li class="page-item @(p == quotes.PageNumber ? "active" : "")">
|
||||
<a class="page-link" href="@QuotePageUrl(p)">@p</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item @(quotes.PageNumber == quotes.TotalPages ? "disabled" : "")">
|
||||
<a class="page-link" href="@QuotePageUrl(quotes.PageNumber + 1)">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,375 @@
|
||||
@model PowderCoating.Application.DTOs.Customer.CreateCustomerDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Add New Customer";
|
||||
ViewData["PageIcon"] = "bi-person-plus";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form asp-action="Create" method="post">
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<!-- Company Information Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-building me-2 text-primary"></i>Company Information</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Company Information"
|
||||
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Leave it blank for individual (non-business) customers. Customer Type controls which features are available — Commercial customers get payment terms, credit limits, and pricing tier discounts; Individual customers are for simpler one-off work.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label asp-for="CompanyName" class="form-label">Company Name</label>
|
||||
<input asp-for="CompanyName" class="form-control" placeholder="Enter company name" />
|
||||
<span asp-validation-for="CompanyName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="IsCommercial" class="form-label">Customer Type
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Customer Type"
|
||||
data-bs-content="Commercial: businesses with ongoing work, purchase orders, and invoicing. Individual: walk-in customers or one-off jobs. This affects which fields are shown and whether pricing tier discounts apply.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
<select asp-for="IsCommercial" class="form-select">
|
||||
<option value="false">Individual</option>
|
||||
<option value="true">Commercial</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-person me-2 text-primary"></i>Contact Information
|
||||
</h5>
|
||||
<div class="alert alert-info alert-permanent py-2 px-3 mb-3" style="font-size:.875rem;">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Required:</strong> At least one of Company Name, First Name, or Last Name — and at least one of Email or Phone.
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ContactFirstName" class="form-label">First Name <span class="text-muted fw-normal">(required if no company name)</span></label>
|
||||
<input asp-for="ContactFirstName" class="form-control" placeholder="Enter first name" />
|
||||
<span asp-validation-for="ContactFirstName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ContactLastName" class="form-label">Last Name</label>
|
||||
<input asp-for="ContactLastName" class="form-control" placeholder="Enter last name" />
|
||||
<span asp-validation-for="ContactLastName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Email" class="form-label">Email <span class="text-danger">*</span> <span class="text-muted fw-normal">(required if no phone number)</span></label>
|
||||
<input asp-for="Email" type="email" class="form-control" placeholder="name@example.com" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="Phone" class="form-label">Phone <span class="text-danger">*</span> <span class="text-muted fw-normal">(required if no email)</span></label>
|
||||
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
|
||||
<span asp-validation-for="Phone" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="MobilePhone" class="form-label">Mobile Phone</label>
|
||||
<input asp-for="MobilePhone" type="tel" class="form-control" placeholder="(555) 123-4567" />
|
||||
<span asp-validation-for="MobilePhone" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label asp-for="Address" class="form-label">Street Address</label>
|
||||
<input asp-for="Address" class="form-control" placeholder="Enter street address" />
|
||||
<span asp-validation-for="Address" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label asp-for="City" class="form-label">City</label>
|
||||
<input asp-for="City" class="form-control" placeholder="Enter city" />
|
||||
<span asp-validation-for="City" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="State" class="form-label">State</label>
|
||||
<input asp-for="State" class="form-control" placeholder="Enter state" />
|
||||
<span asp-validation-for="State" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label asp-for="ZipCode" class="form-label">Zip Code</label>
|
||||
<input asp-for="ZipCode" class="form-control" placeholder="12345" />
|
||||
<span asp-validation-for="ZipCode" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label asp-for="Country" class="form-label">Country</label>
|
||||
<select asp-for="Country" class="form-select">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="USA">USA</option>
|
||||
<option value="Canada">Canada</option>
|
||||
<option value="Mexico">Mexico</option>
|
||||
<option value="Afghanistan">Afghanistan</option>
|
||||
<option value="Albania">Albania</option>
|
||||
<option value="Algeria">Algeria</option>
|
||||
<option value="Argentina">Argentina</option>
|
||||
<option value="Australia">Australia</option>
|
||||
<option value="Austria">Austria</option>
|
||||
<option value="Bangladesh">Bangladesh</option>
|
||||
<option value="Belgium">Belgium</option>
|
||||
<option value="Bolivia">Bolivia</option>
|
||||
<option value="Brazil">Brazil</option>
|
||||
<option value="Chile">Chile</option>
|
||||
<option value="China">China</option>
|
||||
<option value="Colombia">Colombia</option>
|
||||
<option value="Costa Rica">Costa Rica</option>
|
||||
<option value="Croatia">Croatia</option>
|
||||
<option value="Czech Republic">Czech Republic</option>
|
||||
<option value="Denmark">Denmark</option>
|
||||
<option value="Dominican Republic">Dominican Republic</option>
|
||||
<option value="Ecuador">Ecuador</option>
|
||||
<option value="Egypt">Egypt</option>
|
||||
<option value="El Salvador">El Salvador</option>
|
||||
<option value="Finland">Finland</option>
|
||||
<option value="France">France</option>
|
||||
<option value="Germany">Germany</option>
|
||||
<option value="Ghana">Ghana</option>
|
||||
<option value="Greece">Greece</option>
|
||||
<option value="Guatemala">Guatemala</option>
|
||||
<option value="Honduras">Honduras</option>
|
||||
<option value="Hungary">Hungary</option>
|
||||
<option value="India">India</option>
|
||||
<option value="Indonesia">Indonesia</option>
|
||||
<option value="Iran">Iran</option>
|
||||
<option value="Iraq">Iraq</option>
|
||||
<option value="Ireland">Ireland</option>
|
||||
<option value="Israel">Israel</option>
|
||||
<option value="Italy">Italy</option>
|
||||
<option value="Japan">Japan</option>
|
||||
<option value="Jordan">Jordan</option>
|
||||
<option value="Kazakhstan">Kazakhstan</option>
|
||||
<option value="Kenya">Kenya</option>
|
||||
<option value="South Korea">South Korea</option>
|
||||
<option value="Kuwait">Kuwait</option>
|
||||
<option value="Malaysia">Malaysia</option>
|
||||
<option value="Netherlands">Netherlands</option>
|
||||
<option value="New Zealand">New Zealand</option>
|
||||
<option value="Nicaragua">Nicaragua</option>
|
||||
<option value="Nigeria">Nigeria</option>
|
||||
<option value="Norway">Norway</option>
|
||||
<option value="Pakistan">Pakistan</option>
|
||||
<option value="Panama">Panama</option>
|
||||
<option value="Paraguay">Paraguay</option>
|
||||
<option value="Peru">Peru</option>
|
||||
<option value="Philippines">Philippines</option>
|
||||
<option value="Poland">Poland</option>
|
||||
<option value="Portugal">Portugal</option>
|
||||
<option value="Puerto Rico">Puerto Rico</option>
|
||||
<option value="Romania">Romania</option>
|
||||
<option value="Russia">Russia</option>
|
||||
<option value="Saudi Arabia">Saudi Arabia</option>
|
||||
<option value="South Africa">South Africa</option>
|
||||
<option value="Spain">Spain</option>
|
||||
<option value="Sweden">Sweden</option>
|
||||
<option value="Switzerland">Switzerland</option>
|
||||
<option value="Taiwan">Taiwan</option>
|
||||
<option value="Thailand">Thailand</option>
|
||||
<option value="Turkey">Turkey</option>
|
||||
<option value="Ukraine">Ukraine</option>
|
||||
<option value="United Arab Emirates">United Arab Emirates</option>
|
||||
<option value="United Kingdom">United Kingdom</option>
|
||||
<option value="Uruguay">Uruguay</option>
|
||||
<option value="Venezuela">Venezuela</option>
|
||||
<option value="Vietnam">Vietnam</option>
|
||||
</select>
|
||||
<span asp-validation-for="Country" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Information Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-briefcase me-2 text-primary"></i>Business Information</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Business Information"
|
||||
data-bs-content="These fields govern billing and compliance. Payment Terms sets the default for invoices (e.g., Net 30 = payment due within 30 days). Credit Limit is a soft cap on outstanding balance — the system will warn when exceeded. Tax Exempt removes tax from all invoices for this customer (upload the exemption certificate on the Edit page).">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TaxId" class="form-label">Tax ID / EIN</label>
|
||||
<input asp-for="TaxId" class="form-control" placeholder="Enter tax ID" />
|
||||
<span asp-validation-for="TaxId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<label asp-for="PaymentTerms" class="form-label mb-0">Payment Terms</label>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Payment Terms"
|
||||
data-bs-content="Sets the default due date on invoices for this customer. 'Net 30' means payment is due 30 days after the invoice date. This is a default — you can override it on individual invoices.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<select asp-for="PaymentTerms" class="form-select">
|
||||
<option value="">Select payment terms</option>
|
||||
<option value="Net 15">Net 15</option>
|
||||
<option value="Net 30">Net 30</option>
|
||||
<option value="Net 45">Net 45</option>
|
||||
<option value="Net 60">Net 60</option>
|
||||
<option value="Due on Receipt">Due on Receipt</option>
|
||||
<option value="Cash on Delivery">Cash on Delivery</option>
|
||||
</select>
|
||||
<span asp-validation-for="PaymentTerms" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="PricingTierId" class="form-label">Pricing Tier</label>
|
||||
<select asp-for="PricingTierId" asp-items="ViewBag.PricingTiers" class="form-select">
|
||||
<option value="">— No tier —</option>
|
||||
</select>
|
||||
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<label asp-for="CreditLimit" class="form-label mb-0">Credit Limit</label>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Credit Limit"
|
||||
data-bs-content="The maximum outstanding balance you'll extend to this customer before requiring payment. Set to $0 to allow unlimited credit or to require payment upfront. The system will warn (but not block) when the balance exceeds this limit.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="CreditLimit" type="number" step="0.01" min="0" value="0" class="form-control" placeholder="0.00" />
|
||||
</div>
|
||||
<span asp-validation-for="CreditLimit" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="IsTaxExempt" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsTaxExempt" class="form-check-label">Tax Exempt Customer</label>
|
||||
</div>
|
||||
<small class="text-muted">Check this box if the customer is tax exempt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label asp-for="GeneralNotes" class="form-label">General Notes</label>
|
||||
<textarea asp-for="GeneralNotes" class="form-control" rows="4" placeholder="Enter any additional notes about this customer"></textarea>
|
||||
<span asp-validation-for="GeneralNotes" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Preferences -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-bell me-2 text-primary"></i>Notification Preferences</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Notification Preferences"
|
||||
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent and a mobile phone number.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<!-- Email -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="NotifyByEmail" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label asp-for="NotifyByEmail" class="form-check-label">
|
||||
<i class="bi bi-envelope me-1"></i>Email Notifications
|
||||
</label>
|
||||
<div class="form-text">Receive quote and job status updates by email.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ViewBag.SmsEnabled == true)
|
||||
{
|
||||
<!-- SMS Consent (TCPA compliance) -->
|
||||
<div class="mt-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning-subtle text-warning-emphasis d-flex align-items-center gap-2 py-2">
|
||||
<i class="bi bi-shield-exclamation fs-5"></i>
|
||||
<span class="fw-semibold">SMS Consent Requirement (TCPA)</span>
|
||||
<span class="badge bg-warning text-dark ms-auto">Required before enabling SMS</span>
|
||||
</div>
|
||||
<div class="card-body pb-2">
|
||||
<p class="mb-2">
|
||||
Federal law (TCPA) requires <strong>explicit prior written or verbal consent</strong> before sending SMS messages to a customer.
|
||||
Before enabling SMS notifications, you must:
|
||||
</p>
|
||||
<ol class="mb-2 ps-3">
|
||||
<li>Inform the customer they will receive automated text messages for job updates and pickup alerts.</li>
|
||||
<li>Inform them that message and data rates may apply.</li>
|
||||
<li>Explain they can reply <strong>STOP</strong> at any time to opt out.</li>
|
||||
<li>Obtain their clear verbal or written agreement.</li>
|
||||
</ol>
|
||||
<p class="mb-2 small text-muted">
|
||||
Only check the box below <strong>after</strong> the customer has given consent.
|
||||
A confirmation text will be sent automatically to verify enrollment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-secondary bg-body-secondary p-3">
|
||||
<div class="form-check">
|
||||
<input asp-for="SmsConsentGranted" class="form-check-input" type="checkbox" id="SmsConsentGranted" />
|
||||
<label class="form-check-label fw-semibold" for="SmsConsentGranted">
|
||||
<i class="bi bi-shield-check me-1 text-success"></i>
|
||||
Customer has verbally consented to receive SMS notifications
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Checking this box records consent on behalf of the customer and triggers a confirmation text.
|
||||
A mobile phone number must be entered above.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary px-4">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-check-circle me-2"></i>Create Customer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Delete Customer";
|
||||
ViewData["PageIcon"] = "bi-people";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Warning Banner -->
|
||||
<div class="alert alert-danger d-flex align-items-start mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<h5 class="alert-heading mb-2">Are you sure you want to delete this customer?</h5>
|
||||
<p class="mb-0">This action will mark the customer as deleted. All related records (jobs, quotes, notes) will be preserved but the customer will no longer appear in active listings.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-danger shadow-sm">
|
||||
<div class="card-header bg-danger bg-opacity-10 border-danger">
|
||||
<h5 class="mb-0 text-danger">
|
||||
<i class="bi bi-person-x me-2"></i>Customer to be Deleted
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Company Information -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted small text-uppercase mb-2">Company Information</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="text-muted small mb-1">Company Name</label>
|
||||
<p class="fw-semibold mb-0">@Model.CompanyName</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Customer Type</label>
|
||||
<p class="mb-0">
|
||||
@if (Model.IsCommercial)
|
||||
{
|
||||
<span class="badge bg-primary">Commercial</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Individual</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted small text-uppercase mb-2">Contact Information</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Contact Name</label>
|
||||
<p class="mb-0">
|
||||
@if (!string.IsNullOrEmpty(Model.ContactFirstName) || !string.IsNullOrEmpty(Model.ContactLastName))
|
||||
{
|
||||
<span>@Model.ContactFirstName @Model.ContactLastName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not provided</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Email</label>
|
||||
<p class="mb-0">@Model.Email</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Phone</label>
|
||||
<p class="mb-0">@(Model.Phone ?? "Not provided")</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Mobile Phone</label>
|
||||
<p class="mb-0">@(Model.MobilePhone ?? "Not provided")</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Address -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted small text-uppercase mb-2">Address</h6>
|
||||
@if (!string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mb-0">No address provided</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Financial Information -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted small text-uppercase mb-2">Financial Information</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Current Balance</label>
|
||||
<p class="mb-0 fw-semibold @(Model.CurrentBalance > 0 ? "text-danger" : "text-success")">
|
||||
@Model.CurrentBalance.ToString("C")
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Credit Limit</label>
|
||||
<p class="mb-0 fw-semibold">@Model.CreditLimit.ToString("C")</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Payment Terms</label>
|
||||
<p class="mb-0">@(Model.PaymentTerms ?? "Not set")</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.CurrentBalance > 0)
|
||||
{
|
||||
<div class="alert alert-warning d-flex align-items-center">
|
||||
<i class="bi bi-exclamation-circle me-2"></i>
|
||||
<div>
|
||||
<strong>Warning:</strong> This customer has an outstanding balance of @Model.CurrentBalance.ToString("C"). Please ensure all balances are settled before deletion.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Delete Form -->
|
||||
<div class="d-flex gap-2 justify-content-end pt-3 border-top mt-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary px-4">
|
||||
<i class="bi bi-x-circle me-2"></i>Cancel
|
||||
</a>
|
||||
<form asp-action="Delete" method="post" class="d-inline">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<button type="submit" class="btn btn-danger px-4">
|
||||
<i class="bi bi-trash me-2"></i>Delete Customer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3">
|
||||
<i class="bi bi-info-circle me-2 text-info"></i>What happens when you delete a customer?
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
<li>The customer will be marked as deleted (soft delete)</li>
|
||||
<li>They will no longer appear in active customer listings</li>
|
||||
<li>All related jobs, quotes, and notes will be preserved</li>
|
||||
<li>Historical records and reports will still include this customer</li>
|
||||
<li>Administrators can restore deleted customers if needed</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,487 @@
|
||||
@model PowderCoating.Application.DTOs.Customer.CustomerDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = !string.IsNullOrWhiteSpace(Model.CompanyName)
|
||||
? Model.CompanyName
|
||||
: $"{Model.ContactFirstName} {Model.ContactLastName}".Trim();
|
||||
ViewData["PageIcon"] = "bi-person-circle";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-action="Activity" asp-route-id="@Model.Id" class="btn btn-outline-info">
|
||||
<i class="bi bi-clock-history me-2"></i>View Activity
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
|
||||
<i class="bi bi-pencil me-2"></i>Edit
|
||||
</a>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Banner -->
|
||||
<div class="alert @(Model.IsActive ? "alert-success" : "alert-danger") alert-permanent d-flex align-items-center mb-4">
|
||||
<i class="bi @(Model.IsActive ? "bi-check-circle" : "bi-x-circle") me-2" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<strong>Status:</strong> @(Model.IsActive ? "Active Customer" : "Inactive Customer")
|
||||
@if (!Model.IsActive)
|
||||
{
|
||||
<span class="ms-2">- This customer is currently inactive</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Company Information -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-building me-2 text-primary"></i>Company Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="text-muted small mb-1">Company Name</label>
|
||||
<p class="fw-semibold mb-0">@Model.CompanyName</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="text-muted small mb-1">Customer Type</label>
|
||||
<p class="mb-0">
|
||||
@if (Model.IsCommercial)
|
||||
{
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary">
|
||||
<i class="bi bi-building me-1"></i>Commercial
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary">
|
||||
<i class="bi bi-person me-1"></i>Individual
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-person me-2 text-primary"></i>Contact Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Contact Name</label>
|
||||
<p class="mb-0">
|
||||
@if (!string.IsNullOrEmpty(Model.ContactFirstName) || !string.IsNullOrEmpty(Model.ContactLastName))
|
||||
{
|
||||
<span>@Model.ContactFirstName @Model.ContactLastName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not provided</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Email</label>
|
||||
<p class="mb-0">
|
||||
<a href="mailto:@Model.Email" class="text-decoration-none">
|
||||
<i class="bi bi-envelope me-1"></i>@Model.Email
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Phone</label>
|
||||
<p class="mb-0">
|
||||
@if (!string.IsNullOrEmpty(Model.Phone))
|
||||
{
|
||||
<a href="tel:@Model.Phone" class="text-decoration-none">
|
||||
<i class="bi bi-telephone me-1"></i>@Model.Phone
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not provided</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Mobile Phone</label>
|
||||
<p class="mb-0">
|
||||
@if (!string.IsNullOrEmpty(Model.MobilePhone))
|
||||
{
|
||||
<a href="tel:@Model.MobilePhone" class="text-decoration-none">
|
||||
<i class="bi bi-phone me-1"></i>@Model.MobilePhone
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not provided</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
<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>
|
||||
}
|
||||
</p>
|
||||
@if (!string.IsNullOrEmpty(Model.Country))
|
||||
{
|
||||
<p class="mb-0 text-muted">@Model.Country</p>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mb-0">No address provided</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Information -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-briefcase me-2 text-primary"></i>Business Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Tax ID / EIN</label>
|
||||
<p class="mb-0">@(Model.TaxId ?? "Not provided")</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Payment Terms</label>
|
||||
<p class="mb-0">@(Model.PaymentTerms ?? "Not set")</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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Pricing Tier</label>
|
||||
<p class="mb-0">@(Model.PricingTierName ?? "Standard")</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Tax Status</label>
|
||||
<p class="mb-0">
|
||||
@if (Model.IsTaxExempt)
|
||||
{
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> Tax Exempt
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Taxable</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
@if (Model.HasTaxExemptCertificate)
|
||||
{
|
||||
<div class="col-md-12">
|
||||
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
||||
<div class="alert alert-success d-flex justify-content-between align-items-center mb-0 mt-2">
|
||||
<div>
|
||||
<i class="bi bi-file-earmark-check me-2"></i>
|
||||
<strong>File on record:</strong> @Model.TaxExemptCertificateFileName
|
||||
</div>
|
||||
<a asp-action="TaxExemptCertificate" asp-route-id="@Model.Id" class="btn btn-sm btn-outline-dark" target="_blank">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.IsTaxExempt && !Model.HasTaxExemptCertificate)
|
||||
{
|
||||
<div class="col-md-12">
|
||||
<label class="text-muted small mb-1">Tax Exempt Certificate</label>
|
||||
<div class="alert alert-warning mb-0 mt-2">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>No certificate on file.</strong> Please upload a tax exempt certificate to complete the record.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
@if (!string.IsNullOrEmpty(Model.GeneralNotes))
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0" style="white-space: pre-wrap;">@Model.GeneralNotes</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Statistics -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Financial Summary -->
|
||||
<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-currency-dollar me-2 text-primary"></i>Financial Summary
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small mb-1">Outstanding Balance</label>
|
||||
<h3 class="mb-0 @(Model.CurrentBalance > 0 ? "text-danger" : "text-success")">
|
||||
@Model.CurrentBalance.ToString("C")
|
||||
</h3>
|
||||
</div>
|
||||
@if (Model.CreditLimit > 0)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small mb-1">Credit Limit</label>
|
||||
<p class="mb-0 fw-semibold">@Model.CreditLimit.ToString("C")</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small mb-1">Available Credit</label>
|
||||
<p class="mb-0 fw-semibold text-success">
|
||||
@((Model.CreditLimit - Model.CurrentBalance).ToString("C"))
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<hr class="my-2" />
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<label class="text-muted small mb-1">Store Credit Balance</label>
|
||||
<h4 class="mb-0 @(Model.CreditBalance > 0 ? "text-success fw-bold" : "text-muted")">
|
||||
@Model.CreditBalance.ToString("C")
|
||||
</h4>
|
||||
<small class="text-muted">Available for future invoices</small>
|
||||
</div>
|
||||
@if (User.IsInRole("SuperAdmin") || User.IsInRole("Administrator"))
|
||||
{
|
||||
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addCreditModal">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Credit
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store Credit History -->
|
||||
@{
|
||||
var creditMemos = ViewBag.CreditMemos as List<PowderCoating.Core.Entities.CreditMemo>;
|
||||
}
|
||||
@if (creditMemos != null && creditMemos.Count > 0)
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-credit-card me-2 text-primary"></i>Store Credit History
|
||||
</h5>
|
||||
</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">Memo #</th>
|
||||
<th>Issued</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end pe-3">Remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var memo in creditMemos)
|
||||
{
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<span class="fw-semibold small">@memo.MemoNumber</span>
|
||||
<div class="text-muted" style="font-size:0.75rem;">@memo.Reason</div>
|
||||
</td>
|
||||
<td class="small text-muted align-middle">@memo.IssueDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy")</td>
|
||||
<td class="text-end align-middle small">@memo.Amount.ToString("C")</td>
|
||||
<td class="text-end pe-3 align-middle">
|
||||
@if (memo.RemainingBalance > 0)
|
||||
{
|
||||
<span class="badge bg-success">@memo.RemainingBalance.ToString("C")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Used</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Activity -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-clock-history me-2 text-primary"></i>Activity
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small mb-1">Last Contact</label>
|
||||
<p class="mb-0">
|
||||
@if (Model.LastContactDate.HasValue)
|
||||
{
|
||||
<span>@Model.LastContactDate.Value.ToString("MMMM dd, yyyy")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">No contact recorded</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small mb-1">Customer Since</label>
|
||||
<p class="mb-0">@Model.CreatedAt.ToString("MMMM dd, yyyy")</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-lightning me-2 text-primary"></i>Quick Actions
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil me-2"></i>Edit Customer
|
||||
</a>
|
||||
<a asp-action="JobHistory" asp-route-id="@Model.Id" class="btn btn-outline-success">
|
||||
<i class="bi bi-clock-history me-2"></i>Job History
|
||||
</a>
|
||||
<a asp-action="Invoices" asp-route-id="@Model.Id" class="btn btn-outline-warning">
|
||||
<i class="bi bi-receipt me-2"></i>View Invoices
|
||||
</a>
|
||||
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@Model.Id" class="btn btn-outline-success">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Job
|
||||
</a>
|
||||
<a asp-controller="Quotes" asp-action="Create" asp-route-customerId="@Model.Id" class="btn btn-outline-info">
|
||||
<i class="bi bi-file-text me-2"></i>New Quote
|
||||
</a>
|
||||
@if (User.IsInRole("SuperAdmin") || User.IsInRole("Administrator"))
|
||||
{
|
||||
<button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#addCreditModal">
|
||||
<i class="bi bi-wallet2 me-2"></i>Add Store Credit
|
||||
</button>
|
||||
}
|
||||
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-outline-danger">
|
||||
<i class="bi bi-trash me-2"></i>Delete Customer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Store Credit Modal -->
|
||||
@if (User.IsInRole("SuperAdmin") || User.IsInRole("Administrator"))
|
||||
{
|
||||
<div class="modal fade" id="addCreditModal" tabindex="-1" aria-labelledby="addCreditModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form asp-action="AddCredit" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addCreditModalLabel">
|
||||
<i class="bi bi-wallet2 me-2 text-success"></i>Add Store Credit
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Credits can be applied to any future invoice for this customer.
|
||||
Current balance: <strong class="text-success">@Model.CreditBalance.ToString("C")</strong>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Amount <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" name="Amount" class="form-control" step="0.01" min="0.01" max="99999.99" required placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Reason <span class="text-danger">*</span></label>
|
||||
<select name="Reason" class="form-select" required id="creditReasonSelect">
|
||||
<option value="">— Select reason —</option>
|
||||
<option value="Pre-payment / Deposit">Pre-payment / Deposit</option>
|
||||
<option value="Gift / Gift Card">Gift / Gift Card</option>
|
||||
<option value="Overpayment credit">Overpayment credit</option>
|
||||
<option value="Goodwill credit">Goodwill credit</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
<textarea name="Notes" class="form-control" rows="2" maxlength="1000" placeholder="Optional details..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Expiry Date <span class="text-muted fw-normal">(optional)</span></label>
|
||||
<input type="date" name="ExpiryDate" class="form-control" />
|
||||
<div class="form-text">Leave blank for no expiry.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Credit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
@model PowderCoating.Application.DTOs.Customer.UpdateCustomerDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Customer";
|
||||
ViewData["PageIcon"] = "bi-pencil-square";
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form asp-action="Edit" method="post">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<!-- Company Information Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-building me-2 text-primary"></i>Company Information</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Company Information"
|
||||
data-bs-content="Company Name is used on quotes, invoices, and correspondence. Customer Type controls which features are available — Commercial customers get payment terms, credit limits, and pricing tier discounts. Status Inactive hides the customer from new quote/job dropdowns but preserves all history.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="CompanyName" class="form-label">Company Name</label>
|
||||
<input asp-for="CompanyName" class="form-control" placeholder="Enter company name" />
|
||||
<span asp-validation-for="CompanyName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="IsCommercial" class="form-label">Customer Type
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Customer Type"
|
||||
data-bs-content="Commercial: businesses with ongoing work, purchase orders, and invoicing. Individual: walk-in customers or one-off jobs. This affects which fields are shown and whether pricing tier discounts apply.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
<select asp-for="IsCommercial" class="form-select">
|
||||
<option value="false">Individual</option>
|
||||
<option value="true">Commercial</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="IsActive" class="form-label">Status</label>
|
||||
<select asp-for="IsActive" class="form-select">
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-person me-2 text-primary"></i>Contact Information
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ContactFirstName" class="form-label">First Name</label>
|
||||
<input asp-for="ContactFirstName" class="form-control" placeholder="Enter first name" />
|
||||
<span asp-validation-for="ContactFirstName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ContactLastName" class="form-label">Last Name</label>
|
||||
<input asp-for="ContactLastName" class="form-control" placeholder="Enter last name" />
|
||||
<span asp-validation-for="ContactLastName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Email" class="form-label">Email</label>
|
||||
<input asp-for="Email" type="email" class="form-control" placeholder="name@example.com" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="Phone" class="form-label">Phone</label>
|
||||
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
|
||||
<span asp-validation-for="Phone" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="MobilePhone" class="form-label">Mobile Phone</label>
|
||||
<input asp-for="MobilePhone" type="tel" class="form-control" placeholder="(555) 123-4567" />
|
||||
<span asp-validation-for="MobilePhone" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label asp-for="Address" class="form-label">Street Address</label>
|
||||
<input asp-for="Address" class="form-control" placeholder="Enter street address" />
|
||||
<span asp-validation-for="Address" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label asp-for="City" class="form-label">City</label>
|
||||
<input asp-for="City" class="form-control" placeholder="Enter city" />
|
||||
<span asp-validation-for="City" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="State" class="form-label">State</label>
|
||||
<input asp-for="State" class="form-control" placeholder="Enter state" />
|
||||
<span asp-validation-for="State" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label asp-for="ZipCode" class="form-label">Zip Code</label>
|
||||
<input asp-for="ZipCode" class="form-control" placeholder="12345" />
|
||||
<span asp-validation-for="ZipCode" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label asp-for="Country" class="form-label">Country</label>
|
||||
<select asp-for="Country" class="form-select">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="USA">USA</option>
|
||||
<option value="Canada">Canada</option>
|
||||
<option value="Mexico">Mexico</option>
|
||||
<option value="Afghanistan">Afghanistan</option>
|
||||
<option value="Albania">Albania</option>
|
||||
<option value="Algeria">Algeria</option>
|
||||
<option value="Argentina">Argentina</option>
|
||||
<option value="Australia">Australia</option>
|
||||
<option value="Austria">Austria</option>
|
||||
<option value="Bangladesh">Bangladesh</option>
|
||||
<option value="Belgium">Belgium</option>
|
||||
<option value="Bolivia">Bolivia</option>
|
||||
<option value="Brazil">Brazil</option>
|
||||
<option value="Chile">Chile</option>
|
||||
<option value="China">China</option>
|
||||
<option value="Colombia">Colombia</option>
|
||||
<option value="Costa Rica">Costa Rica</option>
|
||||
<option value="Croatia">Croatia</option>
|
||||
<option value="Czech Republic">Czech Republic</option>
|
||||
<option value="Denmark">Denmark</option>
|
||||
<option value="Dominican Republic">Dominican Republic</option>
|
||||
<option value="Ecuador">Ecuador</option>
|
||||
<option value="Egypt">Egypt</option>
|
||||
<option value="El Salvador">El Salvador</option>
|
||||
<option value="Finland">Finland</option>
|
||||
<option value="France">France</option>
|
||||
<option value="Germany">Germany</option>
|
||||
<option value="Ghana">Ghana</option>
|
||||
<option value="Greece">Greece</option>
|
||||
<option value="Guatemala">Guatemala</option>
|
||||
<option value="Honduras">Honduras</option>
|
||||
<option value="Hungary">Hungary</option>
|
||||
<option value="India">India</option>
|
||||
<option value="Indonesia">Indonesia</option>
|
||||
<option value="Iran">Iran</option>
|
||||
<option value="Iraq">Iraq</option>
|
||||
<option value="Ireland">Ireland</option>
|
||||
<option value="Israel">Israel</option>
|
||||
<option value="Italy">Italy</option>
|
||||
<option value="Japan">Japan</option>
|
||||
<option value="Jordan">Jordan</option>
|
||||
<option value="Kazakhstan">Kazakhstan</option>
|
||||
<option value="Kenya">Kenya</option>
|
||||
<option value="South Korea">South Korea</option>
|
||||
<option value="Kuwait">Kuwait</option>
|
||||
<option value="Malaysia">Malaysia</option>
|
||||
<option value="Netherlands">Netherlands</option>
|
||||
<option value="New Zealand">New Zealand</option>
|
||||
<option value="Nicaragua">Nicaragua</option>
|
||||
<option value="Nigeria">Nigeria</option>
|
||||
<option value="Norway">Norway</option>
|
||||
<option value="Pakistan">Pakistan</option>
|
||||
<option value="Panama">Panama</option>
|
||||
<option value="Paraguay">Paraguay</option>
|
||||
<option value="Peru">Peru</option>
|
||||
<option value="Philippines">Philippines</option>
|
||||
<option value="Poland">Poland</option>
|
||||
<option value="Portugal">Portugal</option>
|
||||
<option value="Puerto Rico">Puerto Rico</option>
|
||||
<option value="Romania">Romania</option>
|
||||
<option value="Russia">Russia</option>
|
||||
<option value="Saudi Arabia">Saudi Arabia</option>
|
||||
<option value="South Africa">South Africa</option>
|
||||
<option value="Spain">Spain</option>
|
||||
<option value="Sweden">Sweden</option>
|
||||
<option value="Switzerland">Switzerland</option>
|
||||
<option value="Taiwan">Taiwan</option>
|
||||
<option value="Thailand">Thailand</option>
|
||||
<option value="Turkey">Turkey</option>
|
||||
<option value="Ukraine">Ukraine</option>
|
||||
<option value="United Arab Emirates">United Arab Emirates</option>
|
||||
<option value="United Kingdom">United Kingdom</option>
|
||||
<option value="Uruguay">Uruguay</option>
|
||||
<option value="Venezuela">Venezuela</option>
|
||||
<option value="Vietnam">Vietnam</option>
|
||||
</select>
|
||||
<span asp-validation-for="Country" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Information Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-briefcase me-2 text-primary"></i>Business Information</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Business Information"
|
||||
data-bs-content="Payment Terms sets the default due date on invoices (e.g., Net 30 = 30 days from invoice date). Credit Limit is a soft warning cap — the system alerts when exceeded. Tax Exempt removes tax from all invoices; upload the exemption certificate in the Tax Exempt Certificate section below.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TaxId" class="form-label">Tax ID / EIN</label>
|
||||
<input asp-for="TaxId" class="form-control" placeholder="Enter tax ID" />
|
||||
<span asp-validation-for="TaxId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="PaymentTerms" class="form-label">Payment Terms</label>
|
||||
<select asp-for="PaymentTerms" class="form-select">
|
||||
<option value="">Select payment terms</option>
|
||||
<option value="Net 15">Net 15</option>
|
||||
<option value="Net 30">Net 30</option>
|
||||
<option value="Net 45">Net 45</option>
|
||||
<option value="Net 60">Net 60</option>
|
||||
<option value="Due on Receipt">Due on Receipt</option>
|
||||
<option value="Cash on Delivery">Cash on Delivery</option>
|
||||
</select>
|
||||
<span asp-validation-for="PaymentTerms" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="PricingTierId" class="form-label">Pricing Tier</label>
|
||||
<select asp-for="PricingTierId" asp-items="ViewBag.PricingTiers" class="form-select">
|
||||
<option value="">— No tier —</option>
|
||||
</select>
|
||||
<small class="text-muted">Applies a discount to all quotes for this customer.</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="CreditLimit" class="form-label">Credit Limit</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="CreditLimit" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
|
||||
</div>
|
||||
<span asp-validation-for="CreditLimit" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="IsTaxExempt" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsTaxExempt" class="form-check-label">Tax Exempt Customer</label>
|
||||
</div>
|
||||
<small class="text-muted">Check this box if the customer is tax exempt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label asp-for="GeneralNotes" class="form-label">General Notes</label>
|
||||
<textarea asp-for="GeneralNotes" class="form-control" rows="4" placeholder="Enter any additional notes about this customer"></textarea>
|
||||
<span asp-validation-for="GeneralNotes" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Preferences -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-bell me-2 text-primary"></i>Notification Preferences</h5>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Notification Preferences"
|
||||
data-bs-content="Controls when the customer receives automatic updates. Email notifications send status change alerts (e.g., job ready for pickup) to the customer's email address. SMS requires separate TCPA consent — uncheck 'SMS Notifications Active' to temporarily pause without revoking consent.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<!-- Email -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="NotifyByEmail" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label asp-for="NotifyByEmail" class="form-check-label">
|
||||
<i class="bi bi-envelope me-1"></i>Email Notifications
|
||||
</label>
|
||||
<div class="form-text">Receive quote and job status updates by email.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ViewBag.SmsEnabled == true)
|
||||
{
|
||||
<!-- SMS: show consent status or consent capture depending on existing consent -->
|
||||
<div class="mt-3">
|
||||
@if (Model.SmsConsentedAt.HasValue)
|
||||
{
|
||||
<!-- Consent already recorded — show status and allow pause/resume -->
|
||||
<div class="card border-success bg-success-subtle p-3 mb-2">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<i class="bi bi-shield-fill-check text-success fs-4 mt-1"></i>
|
||||
<div class="flex-fill">
|
||||
<div class="fw-semibold text-success mb-1">SMS Consent Recorded</div>
|
||||
<div class="small text-body-secondary">
|
||||
Consent method: <strong>@Model.SmsConsentMethod</strong><br />
|
||||
Recorded: <strong>@Model.SmsConsentedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy 'at' h:mm tt")</strong>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="NotifyBySms" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label asp-for="NotifyBySms" class="form-check-label">
|
||||
<i class="bi bi-phone me-1"></i>SMS Notifications Active
|
||||
</label>
|
||||
<div class="form-text">Uncheck to temporarily pause SMS without revoking consent.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- No consent on file — show the compliance notice and consent checkbox -->
|
||||
<div class="alert alert-warning border-warning alert-permanent" role="alert">
|
||||
<h6 class="alert-heading fw-bold mb-2">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>SMS Consent Requirement (TCPA)
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
Federal law (TCPA) requires <strong>explicit prior verbal or written consent</strong> before sending SMS messages.
|
||||
Before enabling SMS notifications, you must:
|
||||
</p>
|
||||
<ol class="mb-2 ps-3">
|
||||
<li>Inform the customer they will receive automated texts for job updates and pickup alerts.</li>
|
||||
<li>Inform them that message and data rates may apply.</li>
|
||||
<li>Explain they can reply <strong>STOP</strong> at any time to opt out.</li>
|
||||
<li>Obtain their clear verbal or written agreement.</li>
|
||||
</ol>
|
||||
<p class="mb-0 small text-muted">
|
||||
Only check the box below <strong>after</strong> the customer has given consent.
|
||||
A confirmation text will be sent automatically to verify enrollment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-secondary bg-body-secondary p-3">
|
||||
<div class="form-check">
|
||||
<input asp-for="SmsConsentGranted" class="form-check-input" type="checkbox" id="SmsConsentGranted" />
|
||||
<label class="form-check-label fw-semibold" for="SmsConsentGranted">
|
||||
<i class="bi bi-shield-check me-1 text-success"></i>
|
||||
Customer has verbally consented to receive SMS notifications
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Checking this box records consent on behalf of the customer and triggers a confirmation text.
|
||||
A mobile phone number must be entered above.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary px-4">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax Exempt Certificate Section (Outside main form) -->
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-file-earmark-check me-2 text-primary"></i>Tax Exempt Certificate
|
||||
</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
@if (Model.HasTaxExemptCertificate)
|
||||
{
|
||||
<div class="alert alert-success d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-file-earmark-check me-2"></i>
|
||||
<strong>Certificate on file:</strong> @Model.TaxExemptCertificateFileName
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a asp-action="TaxExemptCertificate" asp-route-id="@Model.Id" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</a>
|
||||
<form asp-action="DeleteTaxExemptCertificate" asp-route-id="@Model.Id" method="post" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this certificate?');">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted">No tax exempt certificate on file.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form asp-action="UploadTaxExemptCertificate" asp-route-id="@Model.Id" method="post" enctype="multipart/form-data">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Upload New Certificate</label>
|
||||
<input type="file" name="certificateFile" class="form-control" accept=".pdf,.jpg,.jpeg,.png" />
|
||||
<small class="text-muted">Accepted formats: PDF, JPG, PNG (Max 10 MB)</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-upload"></i> Upload Certificate
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
@model PagedResult<PowderCoating.Application.DTOs.Customer.CustomerListDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Customers";
|
||||
ViewData["PageIcon"] = "bi-people";
|
||||
ViewData["PageHelpTitle"] = "Customers";
|
||||
ViewData["PageHelpContent"] = "Customers are companies or individuals who bring in work. Commercial customers get business features like payment terms, credit limits, and pricing tier discounts. Individual customers are typically walk-in or one-off jobs. Balance shown here is the total outstanding across all unpaid invoices.";
|
||||
}
|
||||
|
||||
<div class="pcl-metric-strip">
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "TOTAL", Value: Model.TotalCount.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "ACTIVE", Value: Model.Items.Count(c => c.IsActive).ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "COMMERCIAL", Value: Model.Items.Count(c => c.IsCommercial).ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
<div class="pcl-metric-strip-cell">
|
||||
@await Html.PartialAsync("_Metric", (Label: "TOTAL BALANCE", Value: Model.Items.Sum(c => c.CurrentBalance).ToString("C0"), Delta: (string?)null, DeltaDir: (string?)null))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customers Table Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
||||
<div class="d-flex flex-column flex-sm-row gap-2 w-100 w-lg-auto">
|
||||
<form method="get" class="d-flex flex-column flex-sm-row gap-2 flex-grow-1 flex-lg-grow-0">
|
||||
<div class="input-group" style="max-width: 350px; min-width: 200px;">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="searchTerm" class="form-control border-start-0"
|
||||
placeholder="Search customers..." value="@ViewBag.SearchTerm">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Clear</a>
|
||||
}
|
||||
</form>
|
||||
<a asp-action="Create" class="btn btn-primary text-nowrap">
|
||||
<i class="bi bi-plus-circle me-2"></i>
|
||||
<span class="d-none d-sm-inline">Add Customer</span>
|
||||
<span class="d-inline d-sm-none">Add</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No customers found</h5>
|
||||
<p class="text-muted mb-4">Get started by adding your first customer</p>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Your First Customer
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th sortable="CompanyName" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Company</th>
|
||||
<th sortable="ContactName" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Contact</th>
|
||||
<th sortable="Email" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Email</th>
|
||||
<th sortable="Phone" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Phone</th>
|
||||
<th>Type</th>
|
||||
<th sortable="CurrentBalance" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Balance</th>
|
||||
<th sortable="IsActive" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="customerTable">
|
||||
@foreach (var customer in Model.Items)
|
||||
{
|
||||
<tr class="customer-row" data-customer-id="@customer.Id" style="cursor: pointer;">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600;">
|
||||
@{
|
||||
var initial = "?";
|
||||
if (!string.IsNullOrEmpty(customer.CompanyName) && customer.CompanyName.Length > 0)
|
||||
{
|
||||
initial = customer.CompanyName.Substring(0, 1).ToUpper();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(customer.ContactName) && customer.ContactName.Length > 0)
|
||||
{
|
||||
initial = customer.ContactName.Substring(0, 1).ToUpper();
|
||||
}
|
||||
}
|
||||
@initial
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold">@(string.IsNullOrEmpty(customer.CompanyName) ? customer.ContactName ?? "Individual Customer" : customer.CompanyName)</div>
|
||||
@if (customer.LastContactDate.HasValue)
|
||||
{
|
||||
<small class="text-muted">Last contact: @customer.LastContactDate.Value.ToString("MMM dd, yyyy")</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(customer.ContactName))
|
||||
{
|
||||
<span>@customer.ContactName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(customer.Email))
|
||||
{
|
||||
<a href="mailto:@customer.Email" class="text-decoration-none">
|
||||
<i class="bi bi-envelope me-1"></i>@customer.Email
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(customer.Phone))
|
||||
{
|
||||
<a href="tel:@customer.Phone" class="text-decoration-none">
|
||||
<i class="bi bi-telephone me-1"></i>@customer.Phone
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@await Html.PartialAsync("_StatusChip", (Kind: customer.IsCommercial ? "cool" : "neutral", Text: customer.IsCommercial ? "Commercial" : "Individual"))
|
||||
</td>
|
||||
<td>
|
||||
<span class="@(customer.CurrentBalance > 0 ? "text-danger fw-semibold" : "text-success")">
|
||||
@customer.CurrentBalance.ToString("C")
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.Active(customer.IsActive), Text: customer.IsActive ? "Active" : "Inactive"))
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-action="Details" asp-route-id="@customer.Id" class="btn btn-outline-primary" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@customer.Id" class="btn btn-outline-warning" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a asp-action="Delete" asp-route-id="@customer.Id" class="btn btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Card View -->
|
||||
<div class="mobile-card-view">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No customers found</h5>
|
||||
<p class="text-muted mb-4">Get started by adding your first customer</p>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Your First Customer
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mobile-card-list">
|
||||
@foreach (var customer in Model.Items)
|
||||
{
|
||||
<div class="mobile-data-card"
|
||||
data-id="@customer.Id"
|
||||
onclick="window.location.href='@Url.Action("Details", new { id = customer.Id })'">
|
||||
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="bi bi-@(customer.IsCommercial ? "building" : "person")"></i>
|
||||
</div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@(string.IsNullOrEmpty(customer.CompanyName) ? customer.ContactName ?? "Individual Customer" : customer.CompanyName)</h6>
|
||||
<small>@customer.Email</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-card-body">
|
||||
@if (!string.IsNullOrEmpty(customer.ContactName))
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Contact</span>
|
||||
<span class="mobile-card-value">@customer.ContactName</span>
|
||||
</div>
|
||||
}
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Phone</span>
|
||||
<span class="mobile-card-value">@(customer.Phone ?? "—")</span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Type</span>
|
||||
<span class="mobile-card-value">
|
||||
@if (customer.IsCommercial)
|
||||
{
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary">
|
||||
<i class="bi bi-building me-1"></i>Commercial
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary">
|
||||
<i class="bi bi-person me-1"></i>Individual
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Balance</span>
|
||||
<span class="mobile-card-value @(customer.CurrentBalance > 0 ? "text-danger fw-semibold" : "text-success")">
|
||||
@customer.CurrentBalance.ToString("C")
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Status</span>
|
||||
<span class="mobile-card-value">
|
||||
@if (customer.IsActive)
|
||||
{
|
||||
<span class="badge bg-success bg-opacity-10 text-success">
|
||||
<i class="bi bi-check-circle me-1"></i>Active
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger bg-opacity-10 text-danger">
|
||||
<i class="bi bi-x-circle me-1"></i>Inactive
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (customer.LastContactDate.HasValue)
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Last Contact</span>
|
||||
<span class="mobile-card-value">@customer.LastContactDate.Value.ToString("MMM dd, yyyy")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mobile-card-footer">
|
||||
<a href="@Url.Action("Details", new { id = customer.Id })"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
onclick="event.stopPropagation();">
|
||||
<i class="bi bi-eye me-1"></i>View
|
||||
</a>
|
||||
<a href="@Url.Action("Edit", new { id = customer.Id })"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
onclick="event.stopPropagation();">
|
||||
<i class="bi bi-pencil me-1"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.TotalCount > 0)
|
||||
{
|
||||
@await Html.PartialAsync("_Pagination", Model)
|
||||
}
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Make table rows clickable
|
||||
document.querySelectorAll('.customer-row').forEach(row => {
|
||||
row.addEventListener('click', function(e) {
|
||||
// Don't navigate if clicking on action buttons or links
|
||||
if (e.target.closest('.btn-group') || e.target.closest('a') || e.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = this.getAttribute('data-customer-id');
|
||||
window.location.href = '@Url.Action("Details", "Customers")/' + customerId;
|
||||
});
|
||||
|
||||
// Hover handled by CSS .table tbody tr:hover
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
@model PowderCoating.Application.DTOs.Common.PagedResult<PowderCoating.Application.DTOs.Invoice.InvoiceListDto>
|
||||
@using PowderCoating.Core.Enums
|
||||
@using PowderCoating.Web.Controllers
|
||||
|
||||
@{
|
||||
ViewData["Title"] = $"Invoices - {ViewBag.CustomerName}";
|
||||
ViewData["PageIcon"] = "bi-receipt";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<a asp-controller="Invoices" asp-action="Create" asp-route-customerId="@ViewBag.CustomerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Invoice
|
||||
</a>
|
||||
<a asp-action="Details" asp-route-id="@ViewBag.CustomerId" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to Customer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
@Model.TotalCount invoice(s) total
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-receipt" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No invoices found</h5>
|
||||
<p class="text-muted mb-4">This customer has no invoices yet</p>
|
||||
<a asp-controller="Invoices" asp-action="Create" asp-route-customerId="@ViewBag.CustomerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create Invoice
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th sortable="InvoiceNumber" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Invoice #</th>
|
||||
<th>Job #</th>
|
||||
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
||||
<th sortable="DueDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Due Date</th>
|
||||
<th sortable="Total" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="text-end">Total</th>
|
||||
<th sortable="BalanceDue" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="text-end">Balance Due</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var inv in Model.Items)
|
||||
{
|
||||
<tr style="cursor: pointer;" onclick="window.location.href='@Url.Action("Details", "Invoices", new { id = inv.Id })'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; font-weight: 600;">
|
||||
<i class="bi bi-receipt"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold">@inv.InvoiceNumber</div>
|
||||
<small class="text-muted">@inv.InvoiceDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM dd, yyyy")</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if (inv.JobId.HasValue && !string.IsNullOrEmpty(inv.JobNumber))
|
||||
{
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@inv.JobId"
|
||||
class="text-decoration-none" onclick="event.stopPropagation()">
|
||||
@inv.JobNumber
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted fst-italic">No job</span>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<span class="badge bg-@InvoicesController.GetStatusColorClass(inv.Status)">
|
||||
@InvoicesController.GetStatusDisplay(inv.Status)
|
||||
</span>
|
||||
@if (inv.IsOverdue)
|
||||
{
|
||||
<span class="badge bg-danger ms-1">Overdue</span>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if (inv.DueDate.HasValue)
|
||||
{
|
||||
<span class="@(inv.IsOverdue ? "text-danger fw-semibold" : "")">
|
||||
@inv.DueDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM dd, yyyy")
|
||||
@if (inv.IsOverdue)
|
||||
{
|
||||
<i class="bi bi-exclamation-triangle ms-1"></i>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle text-end fw-semibold">@inv.Total.ToString("C")</td>
|
||||
<td class="align-middle text-end">
|
||||
@if (inv.BalanceDue > 0)
|
||||
{
|
||||
<span class="fw-semibold text-danger">@inv.BalanceDue.ToString("C")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-success fw-semibold">Paid</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end pe-4 align-middle" onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-controller="Invoices" asp-action="Details" asp-route-id="@inv.Id"
|
||||
class="btn btn-outline-primary" title="View Invoice">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-controller="Invoices" asp-action="DownloadPdf" asp-route-id="@inv.Id"
|
||||
class="btn btn-outline-secondary" title="Download PDF">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.TotalCount > 0)
|
||||
{
|
||||
@await Html.PartialAsync("_Pagination", Model)
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,181 @@
|
||||
@model PowderCoating.Application.DTOs.Common.PagedResult<PowderCoating.Application.DTOs.Job.JobListDto>
|
||||
@using PowderCoating.Core.Enums
|
||||
|
||||
@{
|
||||
ViewData["Title"] = $"Job History - {ViewBag.CustomerName}";
|
||||
ViewData["PageIcon"] = "bi-briefcase";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<a asp-action="Create" asp-controller="Jobs" asp-route-customerId="@ViewBag.CustomerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Job
|
||||
</a>
|
||||
<a asp-action="Details" asp-route-id="@ViewBag.CustomerId" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Back to Customer
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<div class="alert alert-info alert-permanent d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-funnel me-2"></i>
|
||||
Showing <strong>@Model.TotalCount</strong> job(s) matching "<strong>@ViewBag.SearchTerm</strong>"
|
||||
</div>
|
||||
<a href="@Url.Action("JobHistory", new { id = ViewBag.CustomerId })" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-x me-1"></i>Clear Filter
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
@Model.TotalCount job(s) total
|
||||
</h5>
|
||||
<form asp-action="JobHistory" asp-route-id="@ViewBag.CustomerId" method="get" class="d-flex gap-2">
|
||||
<div class="input-group" style="width: 350px;">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="searchTerm" class="form-control border-start-0"
|
||||
placeholder="Search by job #, description, PO, status..."
|
||||
value="@ViewBag.SearchTerm"
|
||||
aria-label="Search jobs">
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<a href="@Url.Action("JobHistory", new { id = ViewBag.CustomerId })" class="btn btn-outline-secondary" title="Clear search">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
||||
<h5 class="mt-3 text-muted">No jobs found</h5>
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<p class="text-muted mb-4">No jobs match your search criteria</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted mb-4">This customer has no jobs yet</p>
|
||||
<a asp-action="Create" asp-controller="Jobs" asp-route-customerId="@ViewBag.CustomerId" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create First Job
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th sortable="JobNumber" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Job Number</th>
|
||||
<th>Description</th>
|
||||
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
||||
<th sortable="Priority" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Priority</th>
|
||||
<th sortable="ScheduledDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Scheduled</th>
|
||||
<th sortable="DueDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Due Date</th>
|
||||
<th sortable="FinalPrice" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Price</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var job in Model.Items)
|
||||
{
|
||||
<tr style="cursor: pointer;" onclick="window.location.href='@Url.Action("Details", "Jobs", new { id = job.Id })'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600;">
|
||||
<i class="bi bi-briefcase"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold">@job.JobNumber</div>
|
||||
<small class="text-muted">Created @job.CreatedAt.ToString("MMM dd, yyyy")</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>@job.Description</td>
|
||||
<td>
|
||||
<span class="badge bg-@job.StatusColorClass bg-opacity-10 text-@job.StatusColorClass">
|
||||
@job.StatusDisplayName
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-@job.PriorityColorClass">@job.PriorityDisplayName</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (job.ScheduledDate.HasValue)
|
||||
{
|
||||
<span>@job.ScheduledDate.Value.ToString("MMM dd, yyyy")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not scheduled</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (job.DueDate.HasValue)
|
||||
{
|
||||
var isOverdue = job.DueDate.Value < DateTime.Now && job.StatusCode != "COMPLETED" && job.StatusCode != "READYFORPICKUP" && job.StatusCode != "DELIVERED";
|
||||
<span class="@(isOverdue ? "text-danger fw-semibold" : "")">
|
||||
@job.DueDate.Value.ToString("MMM dd, yyyy")
|
||||
@if (isOverdue)
|
||||
{
|
||||
<i class="bi bi-exclamation-triangle ms-1"></i>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not set</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold">@job.FinalPrice.ToString("C")</span>
|
||||
</td>
|
||||
<td class="text-end pe-4" onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.Id" class="btn btn-outline-primary" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-controller="Jobs" asp-action="Edit" asp-route-id="@job.Id" class="btn btn-outline-warning" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.TotalCount > 0)
|
||||
{
|
||||
@await Html.PartialAsync("_Pagination", Model)
|
||||
}
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function changePageSize(size) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('pageSize', size);
|
||||
url.searchParams.set('pageNumber', '1');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user