427c52a499
The Ready pill passed searchTerm=ReadyForPickup which did a text search — "readyforpickup" (no spaces) never matched the display name "Ready for Pickup". Converted to statusGroup=ready and added the corresponding controller case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1040 lines
55 KiB
Plaintext
1040 lines
55 KiB
Plaintext
@model PagedResult<PowderCoating.Application.DTOs.Job.JobListDto>
|
||
|
||
@{
|
||
ViewData["Title"] = "Jobs";
|
||
ViewData["PageIcon"] = "bi-briefcase";
|
||
ViewData["PageHelpTitle"] = "Jobs";
|
||
ViewData["PageHelpContent"] = "A Job is the active work order once a customer's quote is approved. Jobs track the full lifecycle from intake through coating, curing, quality check, and delivery. Each job can contain multiple items with individual coating specifications. Priority and due date help the shop floor triage workload.";
|
||
}
|
||
|
||
@{
|
||
var _allCount = (int)(ViewBag.AllJobCount ?? 0);
|
||
var _wip = (int)(ViewBag.ActiveCount ?? 0);
|
||
var _done = (int)(ViewBag.CompletedCount ?? 0);
|
||
var _ready = (int)(ViewBag.ReadyCount ?? 0);
|
||
var _overdue = (int)(ViewBag.OverdueCount ?? 0);
|
||
var _value = Model.Items.Sum(j => j.FinalPrice);
|
||
}
|
||
<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: "IN PROGRESS", Value: _wip.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||
</div>
|
||
<div class="pcl-metric-strip-cell">
|
||
@await Html.PartialAsync("_Metric", (Label: "COMPLETED", Value: _done.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||
</div>
|
||
<div class="pcl-metric-strip-cell">
|
||
@await Html.PartialAsync("_Metric", (Label: "OVERDUE", Value: _overdue.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
|
||
</div>
|
||
<div class="pcl-metric-strip-cell">
|
||
@await Html.PartialAsync("_Metric", (Label: "VALUE", Value: _value.ToString("C0"), Delta: (string?)null, DeltaDir: (string?)null))
|
||
</div>
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm as string))
|
||
{
|
||
<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>"
|
||
<small class="text-muted ms-2">(searches job number, description, customer, PO, instructions, status, priority)</small>
|
||
</div>
|
||
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-sm btn-outline-secondary">
|
||
<i class="bi bi-x me-1"></i>Clear Filter
|
||
</a>
|
||
</div>
|
||
}
|
||
@if (!string.IsNullOrEmpty(ViewBag.StatusGroup as string) && ViewBag.StatusGroup != "active" && ViewBag.StatusGroup != "all")
|
||
{
|
||
var groupLabel = ViewBag.StatusGroup == "overdue" ? "Overdue Jobs (past due date)"
|
||
: ViewBag.StatusGroup == "completed" ? "Completed Jobs (completed, ready for pickup & delivered)"
|
||
: (string)ViewBag.StatusGroup;
|
||
<div class="alert alert-warning alert-permanent d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<i class="bi bi-funnel-fill me-2"></i>
|
||
Showing: <strong>@groupLabel</strong> — @Model.TotalCount result@(Model.TotalCount == 1 ? "" : "s")
|
||
</div>
|
||
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-sm btn-outline-secondary">
|
||
<i class="bi bi-x me-1"></i>Back to On Floor
|
||
</a>
|
||
</div>
|
||
}
|
||
|
||
@{
|
||
var _activeGroup = ViewBag.StatusGroup as string;
|
||
var _activeSearch = ViewBag.SearchTerm as string;
|
||
// "all" is the explicit show-everything group (bare URL now redirects to "active")
|
||
var _noFilter = _activeGroup == "all" && string.IsNullOrEmpty(_activeSearch) && string.IsNullOrEmpty(ViewBag.TagFilter as string);
|
||
}
|
||
<!-- Jobs Table Card -->
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header border-0 py-3">
|
||
<div class="d-flex flex-column gap-2">
|
||
<!-- Row 1: search + actions -->
|
||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center">
|
||
<form asp-action="Index" method="get" class="d-flex gap-2 align-items-center flex-grow-1" style="max-width:480px;">
|
||
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
|
||
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
|
||
<div class="input-group">
|
||
<span class="input-group-text border-end-0"><i class="bi bi-search text-muted"></i></span>
|
||
<input type="text" name="searchTerm" class="form-control border-start-0"
|
||
id="jobSearchInput"
|
||
placeholder="Search jobs… /"
|
||
value="@ViewBag.SearchTerm"
|
||
aria-label="Search jobs">
|
||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||
</div>
|
||
@if (!string.IsNullOrEmpty(_activeSearch) || !string.IsNullOrEmpty(ViewBag.TagFilter as string))
|
||
{
|
||
<a href="@Url.Action("Index")" class="btn btn-outline-secondary text-nowrap"><i class="bi bi-x-lg"></i></a>
|
||
}
|
||
</form>
|
||
<div class="d-flex gap-2">
|
||
<div class="btn-group">
|
||
<a asp-action="Create" class="btn btn-primary text-nowrap">
|
||
<i class="bi bi-plus-circle me-1"></i>New Job
|
||
</a>
|
||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||
data-bs-toggle="dropdown" aria-expanded="false">
|
||
<span class="visually-hidden">Toggle Dropdown</span>
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end">
|
||
<li>
|
||
<button class="dropdown-item" type="button" id="fromTemplateBtn">
|
||
<i class="bi bi-layout-text-window-reverse me-2 text-primary"></i>From Template
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<a asp-action="Board" class="btn btn-outline-secondary text-nowrap">
|
||
<i class="bi bi-kanban me-1"></i>Board
|
||
</a>
|
||
<a asp-controller="JobTemplates" asp-action="Index" class="btn btn-outline-secondary text-nowrap" title="Manage job templates">
|
||
<i class="bi bi-layout-text-window-reverse me-1"></i><span class="d-none d-md-inline">Templates</span>
|
||
</a>
|
||
<div class="btn-group">
|
||
<a href="@Url.Action("Blank", "WorkOrder")" target="_blank" class="btn btn-outline-secondary text-nowrap" title="Print a blank work order form">
|
||
<i class="bi bi-printer me-1"></i><span class="d-none d-md-inline">Blank Work Order</span>
|
||
</a>
|
||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||
<span class="visually-hidden">Shop screens</span>
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end">
|
||
<li>
|
||
<a asp-action="ShopDisplay" class="dropdown-item" target="_blank">
|
||
<i class="bi bi-display me-2 text-primary"></i>Shop Display <small class="text-muted">(TV)</small>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a asp-action="ShopMobile" class="dropdown-item">
|
||
<i class="bi bi-phone me-2 text-primary"></i>Shop Mobile
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 2: quick-view pills -->
|
||
<div class="pcl-pill-group">
|
||
<a href="@Url.Action("Index", new { statusGroup = "all" })" class="pcl-pill @(_noFilter ? "active" : "")">
|
||
All <span class="pcl-pill-count">@_allCount</span>
|
||
</a>
|
||
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="pcl-pill @(_activeGroup == "active" ? "active" : "")">
|
||
On floor <span class="pcl-pill-count">@_wip</span>
|
||
</a>
|
||
<a href="@Url.Action("Index", new { statusGroup = "overdue" })" class="pcl-pill @(_activeGroup == "overdue" ? "active" : "")">
|
||
Overdue <span class="pcl-pill-count">@_overdue</span>
|
||
</a>
|
||
<a href="@Url.Action("Index", new { statusGroup = "ready" })" class="pcl-pill @(_activeGroup == "ready" ? "active" : "")">
|
||
Ready <span class="pcl-pill-count">@_ready</span>
|
||
</a>
|
||
<a href="@Url.Action("Index", new { statusGroup = "completed" })" class="pcl-pill @(_activeGroup == "completed" ? "active" : "")">
|
||
Completed <span class="pcl-pill-count">@_done</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 jobs found</h5>
|
||
@if (_allCount > 0)
|
||
{
|
||
<p class="text-muted mb-4">No jobs match your current filter.</p>
|
||
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-outline-secondary">
|
||
<i class="bi bi-x-circle me-2"></i>Clear Filters
|
||
</a>
|
||
}
|
||
else
|
||
{
|
||
<p class="text-muted mb-4">Get started by creating your first job.</p>
|
||
<a asp-action="Create" class="btn btn-primary">
|
||
<i class="bi bi-plus-circle me-2"></i>Create Your 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 th-kicker">Job</th>
|
||
<th class="th-kicker">Customer</th>
|
||
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="th-kicker">Status</th>
|
||
<th sortable="Priority" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="th-kicker">Priority</th>
|
||
<th class="th-kicker">Worker</th>
|
||
<th sortable="ScheduledDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="th-kicker">Scheduled</th>
|
||
<th sortable="DueDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="th-kicker">Due</th>
|
||
<th sortable="FinalPrice" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="th-kicker text-end">Price</th>
|
||
<th class="th-kicker text-end pe-4">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="jobsTable">
|
||
@foreach (var job in Model.Items)
|
||
{
|
||
var isHot = job.DueDate.HasValue && job.DueDate.Value < DateTime.Now
|
||
&& job.StatusCode != "COMPLETED" && job.StatusCode != "READYFORPICKUP"
|
||
&& job.StatusCode != "DELIVERED" && job.StatusCode != "CANCELLED";
|
||
var tipParts = new List<string>();
|
||
if (!string.IsNullOrWhiteSpace(job.Description)) tipParts.Add(job.Description);
|
||
if (!string.IsNullOrWhiteSpace(job.CustomerPO)) tipParts.Add("PO: " + job.CustomerPO);
|
||
var tipText = string.Join(" · ", tipParts);
|
||
<tr class="job-row" data-job-id="@job.Id" style="cursor: pointer;"
|
||
@if (!string.IsNullOrEmpty(tipText)) {
|
||
<text>data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="@Html.Encode(tipText)"</text>
|
||
}>
|
||
<td class="ps-4 @(isHot ? "job-hot-cell" : "")">
|
||
<div>
|
||
<div class="mono fw-500">
|
||
@job.JobNumber
|
||
@if (job.IsReworkJob)
|
||
{
|
||
@await Html.PartialAsync("_StatusChip", (Kind: "warn", Text: "Rework"))
|
||
}
|
||
</div>
|
||
<small class="text-muted">@job.CreatedAt.ToString("MMM dd, yyyy")</small>
|
||
@if (!string.IsNullOrWhiteSpace(job.Tags))
|
||
{
|
||
<div class="mt-1">
|
||
@foreach (var tag in job.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).Where(t => !string.IsNullOrWhiteSpace(t)))
|
||
{
|
||
<a href="@Url.Action("Index", new { tagFilter = tag })" class="pcl-chip pcl-chip-cool me-1 text-decoration-none" onclick="event.stopPropagation();">@tag</a>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</td>
|
||
<td>@job.CustomerName</td>
|
||
<td>
|
||
<span class="pcl-chip pcl-chip-@StatusChipHelper.JobStatus(job.StatusCode) status-badge"
|
||
style="cursor:pointer;"
|
||
data-job-id="@job.Id"
|
||
data-job-number="@job.JobNumber"
|
||
data-status-id="@job.JobStatusId"
|
||
data-status-name="@job.StatusDisplayName"
|
||
data-customer-notify="@job.CustomerNotifyByEmail.ToString().ToLower()"
|
||
data-customer-email="@(string.IsNullOrWhiteSpace(job.CustomerEmail) ? "false" : "true")"
|
||
title="Click to change status">
|
||
<span class="pcl-chip-dot"></span>@job.StatusDisplayName
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span class="pcl-chip pcl-chip-@StatusChipHelper.JobPriority(job.PriorityCode) priority-badge"
|
||
style="cursor:pointer;"
|
||
data-job-id="@job.Id"
|
||
data-job-number="@job.JobNumber"
|
||
data-priority-id="@job.JobPriorityId"
|
||
data-priority-name="@job.PriorityDisplayName"
|
||
title="Click to change priority">
|
||
<span class="pcl-chip-dot"></span>@job.PriorityDisplayName
|
||
</span>
|
||
</td>
|
||
<td>
|
||
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
|
||
{
|
||
<span class="badge bg-success-subtle text-success worker-assignment-badge"
|
||
style="cursor: pointer;"
|
||
data-job-id="@job.Id"
|
||
data-job-number="@job.JobNumber"
|
||
data-worker-id="@job.AssignedUserId"
|
||
data-worker-name="@job.AssignedWorkerName"
|
||
title="Click to change worker">
|
||
<i class="bi bi-person-badge me-1"></i>@job.AssignedWorkerName
|
||
</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-muted worker-assignment-badge"
|
||
style="cursor: pointer;"
|
||
data-job-id="@job.Id"
|
||
data-job-number="@job.JobNumber"
|
||
data-worker-id=""
|
||
data-worker-name=""
|
||
title="Click to assign worker">
|
||
Unassigned
|
||
</span>
|
||
}
|
||
</td>
|
||
<td class="date-cell" onclick="openDatePopover(event, @job.Id, 'scheduledDate', '@(job.ScheduledDate.HasValue ? job.ScheduledDate.Value.ToString("yyyy-MM-dd") : "")')" title="Click to change scheduled date" style="cursor:pointer">
|
||
<span class="date-display" id="sched-@job.Id">
|
||
@if (job.ScheduledDate.HasValue)
|
||
{
|
||
<span>@job.ScheduledDate.Value.ToString("MMM dd, yyyy")</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-muted">Not scheduled</span>
|
||
}
|
||
</span>
|
||
<i class="bi bi-pencil-fill ms-1 text-muted date-edit-icon" style="font-size:0.65rem;opacity:0"></i>
|
||
</td>
|
||
<td class="date-cell" onclick="openDatePopover(event, @job.Id, 'dueDate', '@(job.DueDate.HasValue ? job.DueDate.Value.ToString("yyyy-MM-dd") : "")')" title="Click to change due date" style="cursor:pointer">
|
||
<span class="date-display" id="due-@job.Id">
|
||
@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>
|
||
}
|
||
</span>
|
||
<i class="bi bi-pencil-fill ms-1 text-muted date-edit-icon" style="font-size:0.65rem;opacity:0"></i>
|
||
</td>
|
||
<td class="text-end mono">@job.FinalPrice.ToString("C")</td>
|
||
<td class="text-end pe-4">
|
||
<div class="btn-group btn-group-sm">
|
||
<a 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-action="Edit" asp-route-id="@job.Id" class="btn btn-outline-warning" title="Edit">
|
||
<i class="bi bi-pencil"></i>
|
||
</a>
|
||
<a asp-action="Delete" asp-route-id="@job.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 jobs found</h5>
|
||
@if (_allCount > 0)
|
||
{
|
||
<p class="text-muted mb-4">No jobs match your current filter.</p>
|
||
<a href="@Url.Action("Index", new { statusGroup = "active" })" class="btn btn-outline-secondary">
|
||
<i class="bi bi-x-circle me-2"></i>Clear Filters
|
||
</a>
|
||
}
|
||
else
|
||
{
|
||
<p class="text-muted mb-4">Get started by creating your first job.</p>
|
||
<a asp-action="Create" class="btn btn-primary">
|
||
<i class="bi bi-plus-circle me-2"></i>Create Your First Job
|
||
</a>
|
||
}
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="mobile-card-list">
|
||
@foreach (var job in Model.Items)
|
||
{
|
||
<div class="mobile-data-card"
|
||
data-id="@job.Id"
|
||
onclick="window.location.href='@Url.Action("Details", new { id = job.Id })'">
|
||
|
||
<div class="mobile-card-header">
|
||
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||
<i class="bi bi-briefcase"></i>
|
||
</div>
|
||
<div class="mobile-card-title">
|
||
<h6>@job.JobNumber</h6>
|
||
<small>@job.CustomerName</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mobile-card-body">
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Status</span>
|
||
<span class="mobile-card-value">
|
||
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.JobStatus(job.StatusCode), Text: job.StatusDisplayName))
|
||
</span>
|
||
</div>
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Priority</span>
|
||
<span class="mobile-card-value">
|
||
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.JobPriority(job.PriorityCode), Text: job.PriorityDisplayName))
|
||
</span>
|
||
</div>
|
||
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
|
||
{
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Worker</span>
|
||
<span class="mobile-card-value">
|
||
<i class="bi bi-person-badge me-1 text-muted"></i>@job.AssignedWorkerName
|
||
</span>
|
||
</div>
|
||
}
|
||
@if (job.ScheduledDate.HasValue)
|
||
{
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Scheduled</span>
|
||
<span class="mobile-card-value">@job.ScheduledDate.Value.ToString("MMM dd, yyyy")</span>
|
||
</div>
|
||
}
|
||
@if (job.DueDate.HasValue)
|
||
{
|
||
var isOverdue = job.DueDate.Value < DateTime.Now && job.StatusCode != "COMPLETED" && job.StatusCode != "READYFORPICKUP" && job.StatusCode != "DELIVERED";
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Due Date</span>
|
||
<span class="mobile-card-value @(isOverdue ? "text-danger fw-semibold" : "")">
|
||
@job.DueDate.Value.ToString("MMM dd, yyyy")
|
||
@if (isOverdue)
|
||
{
|
||
<i class="bi bi-exclamation-triangle ms-1"></i>
|
||
}
|
||
</span>
|
||
</div>
|
||
}
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Price</span>
|
||
<span class="mobile-card-value fw-semibold">@job.FinalPrice.ToString("C")</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mobile-card-footer">
|
||
<a href="@Url.Action("Details", new { id = job.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 = job.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)
|
||
}
|
||
@if (Model.Items.Any())
|
||
{
|
||
<div class="pcl-kbd-footer">↑↓ to move · ↵ to open · / to filter</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Antiforgery Token for AJAX -->
|
||
@Html.AntiForgeryToken()
|
||
|
||
<!-- Worker Assignment Modal -->
|
||
<div class="modal fade" id="workerAssignmentModal" tabindex="-1" aria-labelledby="workerAssignmentModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="workerAssignmentModalLabel">
|
||
<i class="bi bi-person-badge me-2"></i>Assign Worker
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Job Number</label>
|
||
<p class="fw-semibold mb-0" id="modalJobNumber"></p>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Current Assignment</label>
|
||
<p class="mb-0" id="modalCurrentWorker"></p>
|
||
</div>
|
||
<div>
|
||
<label for="workerSelect" class="form-label">Select Worker</label>
|
||
<select id="workerSelect" class="form-select">
|
||
<option value="">Not assigned</option>
|
||
@if (ViewBag.Workers != null)
|
||
{
|
||
foreach (var worker in (SelectList)ViewBag.Workers)
|
||
{
|
||
<option value="@worker.Value">@worker.Text</option>
|
||
}
|
||
}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-primary" id="saveWorkerAssignment">
|
||
<i class="bi bi-save me-2"></i>Save Assignment
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Priority Change Modal -->
|
||
<div class="modal fade" id="priorityModal" tabindex="-1" aria-labelledby="priorityModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="priorityModalLabel">
|
||
<i class="bi bi-flag me-2"></i>Change Priority
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Job Number</label>
|
||
<p class="fw-semibold mb-0" id="modalPriorityJobNumber"></p>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Current Priority</label>
|
||
<p class="mb-0" id="modalCurrentPriority"></p>
|
||
</div>
|
||
<div>
|
||
<label for="prioritySelect" class="form-label">Select Priority</label>
|
||
<select id="prioritySelect" class="form-select">
|
||
<option value="">Loading priorities...</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-primary" id="savePriority">
|
||
<i class="bi bi-save me-2"></i>Save Priority
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Change Modal -->
|
||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="statusModalLabel">
|
||
<i class="bi bi-arrow-repeat me-2"></i>Change Status
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Job Number</label>
|
||
<p class="fw-semibold mb-0" id="modalStatusJobNumber"></p>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="text-muted small mb-1">Current Status</label>
|
||
<p class="mb-0" id="modalCurrentStatus"></p>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="statusSelect" class="form-label">Select Status</label>
|
||
<select id="statusSelect" class="form-select">
|
||
<option value="">Loading statuses...</option>
|
||
</select>
|
||
</div>
|
||
<div id="statusModalEmailRow">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch"
|
||
id="statusModalSendEmail"
|
||
@(ViewBag.EmailDefaultOnStatusChange == true ? "checked" : "") />
|
||
<label class="form-check-label small" for="statusModalSendEmail">
|
||
<i class="bi bi-envelope me-1"></i>Notify customer via email
|
||
</label>
|
||
</div>
|
||
<div id="statusModalEmailOptOutNote" class="alert alert-warning alert-permanent py-1 px-2 mt-2 small" style="display:none;">
|
||
<i class="bi bi-bell-slash me-1"></i>This customer has email notifications turned off.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-primary" id="saveStatus">
|
||
<i class="bi bi-save me-2"></i>Save Status
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<script>
|
||
let currentJobId = null;
|
||
let currentJobStatusId = null;
|
||
let currentJobPriorityId = null;
|
||
let jobStatuses = [];
|
||
let jobPriorities = [];
|
||
|
||
// Load job statuses from lookup table
|
||
async function loadJobStatuses() {
|
||
try {
|
||
const response = await fetch('/CompanySettings/GetJobStatuses');
|
||
const data = await response.json();
|
||
|
||
if (Array.isArray(data)) {
|
||
jobStatuses = data.sort((a, b) => a.displayOrder - b.displayOrder);
|
||
populateStatusDropdown();
|
||
} else {
|
||
console.error('Failed to load job statuses:', data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading job statuses:', error);
|
||
}
|
||
}
|
||
|
||
function populateStatusDropdown() {
|
||
const select = document.getElementById('statusSelect');
|
||
select.innerHTML = '';
|
||
|
||
jobStatuses.forEach(status => {
|
||
if (status.isActive) {
|
||
const option = document.createElement('option');
|
||
option.value = status.id;
|
||
option.textContent = status.displayName;
|
||
select.appendChild(option);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Load job priorities from lookup table
|
||
async function loadJobPriorities() {
|
||
try {
|
||
const response = await fetch('/CompanySettings/GetJobPriorities');
|
||
const data = await response.json();
|
||
|
||
if (Array.isArray(data)) {
|
||
jobPriorities = data.sort((a, b) => a.displayOrder - b.displayOrder);
|
||
populatePriorityDropdown();
|
||
} else {
|
||
console.error('Failed to load job priorities:', data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading job priorities:', error);
|
||
}
|
||
}
|
||
|
||
function populatePriorityDropdown() {
|
||
const select = document.getElementById('prioritySelect');
|
||
select.innerHTML = '';
|
||
|
||
jobPriorities.forEach(priority => {
|
||
if (priority.isActive) {
|
||
const option = document.createElement('option');
|
||
option.value = priority.id;
|
||
option.textContent = priority.displayName;
|
||
select.appendChild(option);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Load statuses and priorities on page load
|
||
loadJobStatuses();
|
||
loadJobPriorities();
|
||
|
||
// Row tooltips (description + PO)
|
||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el =>
|
||
new bootstrap.Tooltip(el, { trigger: 'hover' }));
|
||
|
||
// / key focuses search input
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === '/' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
|
||
e.preventDefault();
|
||
document.getElementById('jobSearchInput')?.focus();
|
||
}
|
||
});
|
||
|
||
// Make table rows clickable
|
||
document.querySelectorAll('.job-row').forEach(row => {
|
||
row.addEventListener('click', function(e) {
|
||
// Don't navigate if clicking on action buttons, links, worker badges, priority badges, or status badges
|
||
if (e.target.closest('.btn-group') || e.target.closest('a') || e.target.closest('button') ||
|
||
e.target.closest('.worker-assignment-badge') || e.target.closest('.priority-badge') ||
|
||
e.target.closest('.status-badge') || e.target.closest('.date-cell')) {
|
||
return;
|
||
}
|
||
|
||
const jobId = this.getAttribute('data-job-id');
|
||
window.location.href = '@Url.Action("Details", "Jobs")/' + jobId;
|
||
});
|
||
|
||
// Hover handled by CSS .table tbody tr:hover
|
||
});
|
||
|
||
// Worker assignment modal
|
||
document.querySelectorAll('.worker-assignment-badge').forEach(badge => {
|
||
badge.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
currentJobId = this.getAttribute('data-job-id');
|
||
const jobNumber = this.getAttribute('data-job-number');
|
||
const workerId = this.getAttribute('data-worker-id');
|
||
const workerName = this.getAttribute('data-worker-name');
|
||
|
||
// Update modal content
|
||
document.getElementById('modalJobNumber').textContent = jobNumber;
|
||
document.getElementById('modalCurrentWorker').textContent = workerName || 'Not assigned';
|
||
document.getElementById('workerSelect').value = workerId || '';
|
||
|
||
// Show modal
|
||
const modal = new bootstrap.Modal(document.getElementById('workerAssignmentModal'));
|
||
modal.show();
|
||
});
|
||
});
|
||
|
||
// Save worker assignment
|
||
document.getElementById('saveWorkerAssignment').addEventListener('click', function() {
|
||
const workerId = document.getElementById('workerSelect').value;
|
||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||
|
||
// Send AJAX request
|
||
fetch('@Url.Action("UpdateWorkerAssignment", "Jobs")', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'RequestVerificationToken': token
|
||
},
|
||
body: JSON.stringify({
|
||
jobId: parseInt(currentJobId),
|
||
workerId: workerId || null
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Close modal
|
||
bootstrap.Modal.getInstance(document.getElementById('workerAssignmentModal')).hide();
|
||
|
||
// Reload page to show updated assignment
|
||
location.reload();
|
||
} else {
|
||
showError('Error updating worker assignment: ' + (data.message || 'Unknown error'), 'Update Failed');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showError('An error occurred while updating worker assignment', 'Update Failed');
|
||
});
|
||
});
|
||
|
||
// Priority change modal
|
||
document.querySelectorAll('.priority-badge').forEach(badge => {
|
||
badge.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
currentJobId = this.getAttribute('data-job-id');
|
||
currentJobPriorityId = this.getAttribute('data-priority-id');
|
||
const jobNumber = this.getAttribute('data-job-number');
|
||
const priorityName = this.getAttribute('data-priority-name');
|
||
|
||
// Update modal content
|
||
document.getElementById('modalPriorityJobNumber').textContent = jobNumber;
|
||
document.getElementById('modalCurrentPriority').textContent = priorityName;
|
||
document.getElementById('prioritySelect').value = currentJobPriorityId;
|
||
|
||
// Show modal
|
||
const modal = new bootstrap.Modal(document.getElementById('priorityModal'));
|
||
modal.show();
|
||
});
|
||
});
|
||
|
||
// Save priority
|
||
document.getElementById('savePriority').addEventListener('click', function() {
|
||
const priorityId = document.getElementById('prioritySelect').value;
|
||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||
|
||
// Send AJAX request
|
||
fetch('@Url.Action("UpdateJobPriority", "Jobs")', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'RequestVerificationToken': token
|
||
},
|
||
body: JSON.stringify({
|
||
jobId: parseInt(currentJobId),
|
||
priorityId: parseInt(priorityId)
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Close modal
|
||
bootstrap.Modal.getInstance(document.getElementById('priorityModal')).hide();
|
||
|
||
// Reload page to show updated priority
|
||
location.reload();
|
||
} else {
|
||
showError('Error updating job priority: ' + (data.message || 'Unknown error'), 'Update Failed');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showError('An error occurred while updating job priority', 'Update Failed');
|
||
});
|
||
});
|
||
|
||
// Status change modal
|
||
document.querySelectorAll('.status-badge').forEach(badge => {
|
||
badge.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
currentJobId = this.getAttribute('data-job-id');
|
||
currentJobStatusId = this.getAttribute('data-status-id');
|
||
const jobNumber = this.getAttribute('data-job-number');
|
||
const statusName = this.getAttribute('data-status-name');
|
||
const customerNotify = this.getAttribute('data-customer-notify') !== 'false';
|
||
const customerHasEmail = this.getAttribute('data-customer-email') !== 'false';
|
||
|
||
// Update modal content
|
||
document.getElementById('modalStatusJobNumber').textContent = jobNumber;
|
||
document.getElementById('modalCurrentStatus').textContent = statusName;
|
||
document.getElementById('statusSelect').value = currentJobStatusId;
|
||
|
||
// Show email controls only when the customer has an email address on file
|
||
const emailRow = document.getElementById('statusModalEmailRow');
|
||
if (emailRow) emailRow.style.display = customerHasEmail ? '' : 'none';
|
||
|
||
// Reflect customer email opt-out preference
|
||
const emailCheckbox = document.getElementById('statusModalSendEmail');
|
||
const emailOptOutNote = document.getElementById('statusModalEmailOptOutNote');
|
||
if (emailCheckbox && customerHasEmail) {
|
||
emailCheckbox.disabled = !customerNotify;
|
||
if (!customerNotify) emailCheckbox.checked = false;
|
||
}
|
||
if (emailOptOutNote) emailOptOutNote.style.display = (customerHasEmail && !customerNotify) ? 'block' : 'none';
|
||
|
||
// Show modal
|
||
const modal = new bootstrap.Modal(document.getElementById('statusModal'));
|
||
modal.show();
|
||
});
|
||
});
|
||
|
||
// Save status
|
||
document.getElementById('saveStatus').addEventListener('click', function() {
|
||
const statusId = document.getElementById('statusSelect').value;
|
||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||
const sendEmail = document.getElementById('statusModalSendEmail')?.checked ?? false;
|
||
|
||
// Send AJAX request
|
||
fetch('@Url.Action("UpdateJobStatus", "Jobs")', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'RequestVerificationToken': token
|
||
},
|
||
body: JSON.stringify({
|
||
jobId: parseInt(currentJobId),
|
||
statusId: parseInt(statusId),
|
||
sendEmail: sendEmail
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Close modal
|
||
bootstrap.Modal.getInstance(document.getElementById('statusModal')).hide();
|
||
|
||
// Reload page to show updated status
|
||
location.reload();
|
||
} else {
|
||
showError('Error updating job status: ' + (data.message || 'Unknown error'), 'Update Failed');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showError('An error occurred while updating job status', 'Update Failed');
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.date-cell:hover .date-edit-icon { opacity: 1 !important; }
|
||
.date-cell:hover { background-color: var(--bs-table-hover-bg); }
|
||
</style>
|
||
|
||
<script>
|
||
// ── Inline date editing (job list) ────────────────────────────────────
|
||
let activeDatePopover = null;
|
||
|
||
function openDatePopover(event, jobId, field, currentValue) {
|
||
event.stopPropagation();
|
||
|
||
// Close any already-open popover
|
||
closeDatePopover();
|
||
|
||
const cell = event.currentTarget;
|
||
const label = field === 'scheduledDate' ? 'Scheduled Date' : 'Due Date';
|
||
const allowClear = field === 'dueDate';
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'date-popover shadow rounded border bg-white p-2';
|
||
wrapper.style.cssText = 'position:absolute;z-index:1055;min-width:220px;';
|
||
wrapper.innerHTML = `
|
||
<div class="fw-semibold small text-muted mb-1">${label}</div>
|
||
<div class="input-group input-group-sm">
|
||
<input type="date" class="form-control" id="dp-input" value="${currentValue}">
|
||
<button class="btn btn-primary" onclick="saveDateFromPopover(${jobId},'${field}')" title="Save"><i class="bi bi-check-lg"></i></button>
|
||
<button class="btn btn-outline-secondary" onclick="closeDatePopover()" title="Cancel"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
${allowClear ? `<button class="btn btn-link btn-sm p-0 mt-1 text-danger" onclick="saveDateFromPopover(${jobId},'${field}',true)"><i class="bi bi-x-circle me-1"></i><small>Clear date</small></button>` : ''}
|
||
`;
|
||
|
||
// Position relative to viewport using absolute on body
|
||
document.body.appendChild(wrapper);
|
||
const rect = cell.getBoundingClientRect();
|
||
wrapper.style.top = (rect.bottom + window.scrollY + 4) + 'px';
|
||
wrapper.style.left = (rect.left + window.scrollX) + 'px';
|
||
|
||
activeDatePopover = wrapper;
|
||
wrapper.querySelector('#dp-input').focus();
|
||
|
||
// Close on outside click
|
||
setTimeout(() => document.addEventListener('click', outsideClickHandler), 0);
|
||
}
|
||
|
||
function outsideClickHandler(e) {
|
||
if (activeDatePopover && !activeDatePopover.contains(e.target)) {
|
||
closeDatePopover();
|
||
}
|
||
}
|
||
|
||
function closeDatePopover() {
|
||
if (activeDatePopover) {
|
||
activeDatePopover.remove();
|
||
activeDatePopover = null;
|
||
document.removeEventListener('click', outsideClickHandler);
|
||
}
|
||
}
|
||
|
||
async function saveDateFromPopover(jobId, field, clear = false) {
|
||
const input = document.getElementById('dp-input');
|
||
const value = clear ? '' : (input?.value || '');
|
||
|
||
const saveBtn = activeDatePopover?.querySelector('.btn-primary');
|
||
if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
|
||
|
||
try {
|
||
const resp = await fetch('@Url.Action("UpdateDates", "Jobs")', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? ''
|
||
},
|
||
body: JSON.stringify({
|
||
jobId,
|
||
scheduledDate: field === 'scheduledDate' ? value : undefined,
|
||
dueDate: field === 'dueDate' ? value : undefined
|
||
})
|
||
});
|
||
const result = await resp.json();
|
||
if (!result.success) throw new Error(result.message);
|
||
|
||
// Update display cell in place
|
||
const prefix = field === 'scheduledDate' ? 'sched' : 'due';
|
||
const displayEl = document.getElementById(`${prefix}-${jobId}`);
|
||
if (displayEl) {
|
||
if (value) {
|
||
const d = new Date(value + 'T12:00:00');
|
||
const formatted = d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
|
||
displayEl.innerHTML = `<span>${formatted}</span>`;
|
||
} else {
|
||
const emptyLabel = field === 'scheduledDate' ? 'Not scheduled' : 'Not set';
|
||
displayEl.innerHTML = `<span class="text-muted">${emptyLabel}</span>`;
|
||
}
|
||
}
|
||
|
||
closeDatePopover();
|
||
} catch (err) {
|
||
alert('Could not save date: ' + err.message);
|
||
if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = '<i class="bi bi-check-lg"></i>'; }
|
||
}
|
||
}
|
||
|
||
// ── From Template picker ──────────────────────────────────────────────
|
||
document.getElementById('fromTemplateBtn')?.addEventListener('click', async function () {
|
||
const modal = new bootstrap.Modal(document.getElementById('fromTemplateModal'));
|
||
const list = document.getElementById('templatePickerList');
|
||
list.innerHTML = '<div class="text-center p-4"><div class="spinner-border spinner-border-sm"></div></div>';
|
||
modal.show();
|
||
|
||
try {
|
||
const resp = await fetch('@Url.Action("GetTemplatesJson", "JobTemplates")');
|
||
const templates = await resp.json();
|
||
|
||
if (!templates.length) {
|
||
list.innerHTML = '<p class="text-muted p-3 mb-0">No active templates found. Save a job as a template first.</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = templates.map(t => `
|
||
<a href="@Url.Action("Create", "Jobs")?templateId=${t.id}"
|
||
class="list-group-item list-group-item-action p-3">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<div class="fw-semibold">${t.name}</div>
|
||
${t.description ? `<small class="text-muted">${t.description}</small>` : ''}
|
||
${t.customerName ? `<div class="small text-muted mt-1"><i class="bi bi-building me-1"></i>${t.customerName}</div>` : ''}
|
||
</div>
|
||
<div class="text-end flex-shrink-0 ms-3">
|
||
<span class="badge bg-secondary">${t.items.length} item${t.items.length !== 1 ? 's' : ''}</span>
|
||
${t.usageCount > 0 ? `<div class="small text-muted mt-1">Used ${t.usageCount}×</div>` : ''}
|
||
</div>
|
||
</div>
|
||
</a>`).join('');
|
||
} catch {
|
||
list.innerHTML = '<p class="text-danger p-3 mb-0">Failed to load templates.</p>';
|
||
}
|
||
});
|
||
</script>
|
||
}
|
||
|
||
<!-- From Template Modal -->
|
||
<div class="modal fade" id="fromTemplateModal" tabindex="-1" aria-labelledby="fromTemplateModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="fromTemplateModalLabel">
|
||
<i class="bi bi-layout-text-window-reverse me-2 text-primary"></i>Create Job from Template
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body p-0">
|
||
<div class="list-group list-group-flush" id="templatePickerList"></div>
|
||
</div>
|
||
<div class="modal-footer justify-content-between">
|
||
<a asp-controller="JobTemplates" asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||
<i class="bi bi-gear me-1"></i>Manage Templates
|
||
</a>
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|