61866e1d1e
Non-terminal jobs scheduled for past dates now appear in a red 'Carried Over' section at the top of today's board so they can't silently disappear. Also added alert-permanent to the board tip so the layout doesn't auto-dismiss it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1014 lines
52 KiB
Plaintext
1014 lines
52 KiB
Plaintext
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
|
|
@using PowderCoating.Application.DTOs.Job
|
|
@using PowderCoating.Core.Entities
|
|
@using PowderCoating.Core.Enums
|
|
|
|
@{
|
|
ViewData["Title"] = "Daily Board";
|
|
ViewData["PageIcon"] = "bi-kanban";
|
|
ViewData["PageHelpTitle"] = "Daily Board";
|
|
ViewData["PageHelpContent"] = "Day-by-day view of jobs scheduled for shop work. Drag rows to reorder processing order. Click any Priority badge or Worker cell to quick-edit inline without leaving the page. Navigate between days with Previous/Next. Overdue due dates show in red. The Scheduled Maintenance section at the bottom shows equipment tasks due on the same day.";
|
|
var scheduledDate = ViewBag.ScheduledDate as DateTime? ?? DateTime.Today;
|
|
var maintenanceItems = ViewBag.MaintenanceItems as IEnumerable<MaintenanceRecord> ?? Enumerable.Empty<MaintenanceRecord>();
|
|
var overdueJobs = ViewBag.OverdueJobs as List<JobDailyPriorityDto> ?? new List<JobDailyPriorityDto>();
|
|
}
|
|
|
|
<div class="container-fluid">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<p class="text-muted mb-0">
|
|
<i class="bi bi-calendar3 me-1"></i>
|
|
Scheduled for @scheduledDate.ToString("MMMM dd, yyyy")
|
|
@if (scheduledDate.Date == DateTime.Today)
|
|
{
|
|
<span class="badge bg-primary ms-2">Today</span>
|
|
}
|
|
else if (scheduledDate.Date == DateTime.Today.AddDays(1))
|
|
{
|
|
<span class="badge bg-info ms-2">Tomorrow</span>
|
|
}
|
|
else if (scheduledDate.Date == DateTime.Today.AddDays(-1))
|
|
{
|
|
<span class="badge bg-secondary ms-2">Yesterday</span>
|
|
}
|
|
</p>
|
|
<div class="btn-group" role="group" aria-label="Date navigation">
|
|
<a asp-controller="OvenScheduler" asp-action="Index" asp-route-date="@scheduledDate.ToString("yyyy-MM-dd")"
|
|
class="btn btn-outline-secondary">
|
|
<i class="bi bi-fire me-1"></i>Oven Scheduler
|
|
</a>
|
|
<a href="@Url.Action("Index", new { date = scheduledDate.AddDays(-1) })"
|
|
class="btn btn-outline-primary">
|
|
<i class="bi bi-chevron-left"></i> Previous Day
|
|
</a>
|
|
<a href="@Url.Action("Index")"
|
|
class="btn btn-primary">
|
|
<i class="bi bi-calendar-check"></i> Today
|
|
</a>
|
|
<a href="@Url.Action("Index", new { date = scheduledDate.AddDays(1) })"
|
|
class="btn btn-outline-primary">
|
|
Next Day <i class="bi bi-chevron-right"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
@* ── Carried-Over (Overdue) Jobs ──────────────────────────────────────── *@
|
|
@if (overdueJobs.Any())
|
|
{
|
|
<div class="card border-danger mb-4">
|
|
<div class="card-header bg-danger text-white">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
<strong>Carried Over — Not Yet Completed</strong>
|
|
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
|
data-bs-title="Carried Over Jobs"
|
|
data-bs-content="These jobs were scheduled for a past date but have not reached a terminal status. Reschedule them using the Scheduled Date cell, or complete them to remove them from this list.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
<span class="badge bg-white text-danger">@overdueJobs.Count job@(overdueJobs.Count == 1 ? "" : "s")</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Job Number</th>
|
|
<th>Customer</th>
|
|
<th>Status</th>
|
|
<th>Priority</th>
|
|
<th>Assigned Worker</th>
|
|
<th>Scheduled Date</th>
|
|
<th>Due Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var job in overdueJobs)
|
|
{
|
|
<tr>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
<strong>@job.JobNumber</strong>
|
|
</td>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
@job.CustomerName
|
|
</td>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
<span class="badge bg-@job.StatusColorClass">@job.StatusDisplayName</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber');"
|
|
style="cursor: pointer;" title="Click to change priority">
|
|
<span class="badge bg-@job.PriorityColorClass priority-badge-@job.JobId">
|
|
@job.PriorityDisplayName
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;" title="Click to assign worker">
|
|
<span class="worker-display-@job.JobId">
|
|
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
|
|
{
|
|
<span class="badge bg-info">
|
|
<i class="bi bi-person me-1"></i>@job.AssignedWorkerName
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">Unassigned <i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i></span>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openScheduledDateModal(@job.JobId, '@(job.ScheduledDate?.ToString("yyyy-MM-dd") ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;" title="Click to reschedule">
|
|
<span class="scheduled-date-display-@job.JobId">
|
|
@if (job.ScheduledDate.HasValue)
|
|
{
|
|
<span class="text-danger fw-bold">
|
|
<i class="bi bi-calendar-x me-1"></i>
|
|
@job.ScheduledDate.Value.ToString("MMM dd, yyyy")
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openDueDateModal(@job.JobId, '@(job.DueDate?.ToString("yyyy-MM-dd") ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;" title="Click to change due date">
|
|
<span class="due-date-display-@job.JobId">
|
|
@if (job.DueDate.HasValue)
|
|
{
|
|
var isOverdue = job.DueDate.Value.Date < DateTime.Today;
|
|
<span class="@(isOverdue ? "text-danger fw-bold" : "")">
|
|
<i class="bi bi-calendar-x me-1"></i>
|
|
@job.DueDate.Value.ToString("MMM dd, yyyy")
|
|
@if (isOverdue)
|
|
{
|
|
<i class="bi bi-exclamation-triangle-fill ms-1"></i>
|
|
}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">Not set <i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i></span>
|
|
}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (!Model.Any())
|
|
{
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="card">
|
|
<div class="card-header bg-primary text-white">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-calendar-week me-2"></i>
|
|
<span>Drag and drop to reorder jobs</span>
|
|
<a tabindex="0" class="help-icon text-white opacity-75" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
|
data-bs-title="Reorder & Quick Edit"
|
|
data-bs-content="Drag the ⠿ grip on the left of any row to change processing order — saved automatically. Click a Priority badge to change priority. Click the Worker cell to reassign. Click Scheduled Date or Due Date to update dates inline without navigating away.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0" id="jobsTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 40px;"></th>
|
|
<th>Job Number</th>
|
|
<th>Customer</th>
|
|
<th>Status</th>
|
|
<th>Priority</th>
|
|
<th>Assigned Worker</th>
|
|
<th>Scheduled Date</th>
|
|
<th>Due Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="sortableJobs">
|
|
@foreach (var job in Model)
|
|
{
|
|
<tr class="sortable-row"
|
|
data-job-id="@job.JobId">
|
|
<td class="text-center drag-handle">
|
|
<i class="bi bi-grip-vertical text-muted"></i>
|
|
</td>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
<strong>@job.JobNumber</strong>
|
|
</td>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
@job.CustomerName
|
|
</td>
|
|
<td class="clickable-cell" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
|
<span class="badge bg-@job.StatusColorClass" id="status-badge-@job.JobId">
|
|
@job.StatusDisplayName
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber');"
|
|
style="cursor: pointer;"
|
|
title="Click to change priority">
|
|
<span class="badge bg-@job.PriorityColorClass priority-badge-@job.JobId">
|
|
@job.PriorityDisplayName
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;"
|
|
title="Click to assign worker">
|
|
<span class="worker-display-@job.JobId">
|
|
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
|
|
{
|
|
<span class="badge bg-info">
|
|
<i class="bi bi-person me-1"></i>
|
|
@job.AssignedWorkerName
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">
|
|
Unassigned
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openScheduledDateModal(@job.JobId, '@(job.ScheduledDate?.ToString("yyyy-MM-dd") ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;"
|
|
title="Click to change scheduled date">
|
|
<span class="scheduled-date-display-@job.JobId">
|
|
@if (job.ScheduledDate.HasValue)
|
|
{
|
|
<span>
|
|
<i class="bi bi-calendar-check me-1"></i>
|
|
@job.ScheduledDate.Value.ToString("MMM dd, yyyy")
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">
|
|
Not set
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openDueDateModal(@job.JobId, '@(job.DueDate?.ToString("yyyy-MM-dd") ?? "")', '@job.JobNumber');"
|
|
style="cursor: pointer;"
|
|
title="Click to change due date">
|
|
<span class="due-date-display-@job.JobId">
|
|
@if (job.DueDate.HasValue)
|
|
{
|
|
var isOverdue = job.DueDate.Value.Date < DateTime.Today;
|
|
<span class="@(isOverdue ? "text-danger fw-bold" : "")">
|
|
<i class="bi bi-calendar-x me-1"></i>
|
|
@job.DueDate.Value.ToString("MMM dd, yyyy")
|
|
@if (isOverdue)
|
|
{
|
|
<i class="bi bi-exclamation-triangle-fill ms-1"></i>
|
|
}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">
|
|
Not set
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<div class="alert alert-info alert-permanent border-info shadow-sm" style="position: sticky; bottom: 0; z-index: 1020; margin-bottom: 0;">
|
|
<i class="bi bi-lightbulb me-2"></i>
|
|
<strong>Tip:</strong> Drag rows to reorder. Click priority or worker to quick edit. Click other cells to view job details.
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* ── Scheduled Maintenance for the Day ──────────────────────────────── *@
|
|
<div class="mt-4">
|
|
<div class="card">
|
|
<div class="card-header bg-warning text-dark">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-tools me-2"></i>
|
|
<strong>Scheduled Maintenance</strong>
|
|
<span class="ms-2 text-muted small">@scheduledDate.ToString("MMMM dd, yyyy")</span>
|
|
<a tabindex="0" class="help-icon" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="focus"
|
|
data-bs-title="Scheduled Maintenance"
|
|
data-bs-content="Equipment maintenance tasks scheduled for this day. Click any row to open the full maintenance record. Click the Worker cell to reassign a technician inline. Tasks are color-coded by priority: red = Critical, yellow = High, blue = Normal.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
@if (maintenanceItems.Any())
|
|
{
|
|
<span class="badge bg-dark">@maintenanceItems.Count() item@(maintenanceItems.Count() == 1 ? "" : "s")</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
@if (!maintenanceItems.Any())
|
|
{
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="bi bi-check-circle fs-4 d-block mb-2"></i>
|
|
No maintenance scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Equipment</th>
|
|
<th>Type</th>
|
|
<th>Priority</th>
|
|
<th>Status</th>
|
|
<th>Assigned To</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var item in maintenanceItems)
|
|
{
|
|
var priorityBg = item.Priority switch
|
|
{
|
|
MaintenancePriority.Critical => "danger",
|
|
MaintenancePriority.High => "warning",
|
|
MaintenancePriority.Normal => "info",
|
|
_ => "secondary"
|
|
};
|
|
var priorityFg = (item.Priority == MaintenancePriority.High || item.Priority == MaintenancePriority.Normal)
|
|
? "text-dark" : "text-white";
|
|
var statusBg = item.Status == MaintenanceStatus.InProgress ? "success" : "primary";
|
|
var statusLabel = item.Status == MaintenanceStatus.InProgress ? "In Progress" : "Scheduled";
|
|
<tr onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'"
|
|
style="cursor: pointer;">
|
|
<td>
|
|
<strong>@(item.Equipment?.EquipmentName ?? "—")</strong>
|
|
@if (!string.IsNullOrEmpty(item.Equipment?.Location))
|
|
{
|
|
<br /><small class="text-muted"><i class="bi bi-geo-alt me-1"></i>@item.Equipment.Location</small>
|
|
}
|
|
</td>
|
|
<td>@item.MaintenanceType</td>
|
|
<td>
|
|
<span class="badge bg-@priorityBg @priorityFg">@item.Priority</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-@statusBg">@statusLabel</span>
|
|
</td>
|
|
<td class="editable-cell"
|
|
onclick="event.stopPropagation(); openMaintenanceWorkerModal(@item.Id, '@(item.AssignedUserId ?? "")', '@(item.Equipment?.EquipmentName ?? "Maintenance")');"
|
|
style="cursor: pointer;"
|
|
title="Click to assign worker">
|
|
<span class="maint-worker-display-@item.Id">
|
|
@if (item.AssignedUser != null)
|
|
{
|
|
<span class="badge bg-info text-dark">
|
|
<i class="bi bi-person me-1"></i>@item.AssignedUser.FullName
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">
|
|
Unassigned
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td style="max-width: 300px;">
|
|
<span class="text-muted d-block text-truncate">@item.Description</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Priority 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">Change Priority</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Select priority for <strong id="priorityJobNumber"></strong>:</p>
|
|
<div id="priorityOptions" class="d-grid gap-2">
|
|
<!-- Priority options will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Worker Modal -->
|
|
<div class="modal fade" id="workerModal" tabindex="-1" aria-labelledby="workerModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="workerModalLabel">Assign Worker</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Select worker for <strong id="workerJobNumber"></strong>:</p>
|
|
<div id="workerOptions" class="d-grid gap-2">
|
|
<!-- Worker options will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance Worker Modal -->
|
|
<div class="modal fade" id="maintenanceWorkerModal" tabindex="-1" aria-labelledby="maintenanceWorkerModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="maintenanceWorkerModalLabel">Assign Worker</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Assign worker to <strong id="maintenanceWorkerEquipmentName"></strong>:</p>
|
|
<div id="maintenanceWorkerOptions" class="d-grid gap-2">
|
|
<!-- Worker options populated by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scheduled Date Modal -->
|
|
<div class="modal fade" id="scheduledDateModal" tabindex="-1" aria-labelledby="scheduledDateModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="scheduledDateModalLabel">Change Scheduled Date</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Set scheduled date for <strong id="scheduledDateJobNumber"></strong>:</p>
|
|
<div class="mb-3">
|
|
<label for="scheduledDateInput" class="form-label">Scheduled Date</label>
|
|
<input type="date" class="form-control" id="scheduledDateInput">
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-primary flex-grow-1" onclick="saveScheduledDate()">
|
|
<i class="bi bi-check-circle me-1"></i>Save
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearScheduledDate()">
|
|
<i class="bi bi-x-circle me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Due Date Modal -->
|
|
<div class="modal fade" id="dueDateModal" tabindex="-1" aria-labelledby="dueDateModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="dueDateModalLabel">Change Due Date</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Set due date for <strong id="dueDateJobNumber"></strong>:</p>
|
|
<div class="mb-3">
|
|
<label for="dueDateInput" class="form-label">Due Date</label>
|
|
<input type="date" class="form-control" id="dueDateInput">
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-primary flex-grow-1" onclick="saveDueDate()">
|
|
<i class="bi bi-check-circle me-1"></i>Save
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearDueDate()">
|
|
<i class="bi bi-x-circle me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<!-- Sortable.js for drag-and-drop -->
|
|
<script src="~/lib/sortablejs/Sortable.min.js"></script>
|
|
|
|
<script>
|
|
let currentJobId = null;
|
|
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
|
let priorityModal, workerModal, scheduledDateModal, dueDateModal;
|
|
|
|
// Priority and worker data from server
|
|
const priorities = @Html.Raw(Json.Serialize(ViewBag.PrioritiesJson));
|
|
const workers = @Html.Raw(Json.Serialize(ViewBag.WorkersJson));
|
|
|
|
function openPriorityModal(jobId, currentPriorityId, jobNumber) {
|
|
currentJobId = jobId;
|
|
document.getElementById('priorityJobNumber').textContent = jobNumber;
|
|
|
|
// Build priority options with color-coded badges
|
|
const optionsHtml = priorities.map(p => `
|
|
<button type="button"
|
|
class="btn btn-outline-${p.colorClass} text-start ${p.id === currentPriorityId ? 'active' : ''}"
|
|
onclick="updatePriority(${jobId}, ${p.id})">
|
|
<span class="badge bg-${p.colorClass} me-2">${p.name}</span>
|
|
${p.id === currentPriorityId ? '<i class="bi bi-check-circle-fill float-end"></i>' : ''}
|
|
</button>
|
|
`).join('');
|
|
|
|
document.getElementById('priorityOptions').innerHTML = optionsHtml;
|
|
priorityModal.show();
|
|
}
|
|
|
|
function openWorkerModal(jobId, currentWorkerId, jobNumber) {
|
|
currentJobId = jobId;
|
|
document.getElementById('workerJobNumber').textContent = jobNumber;
|
|
|
|
// Build worker options
|
|
let optionsHtml = `
|
|
<button type="button"
|
|
class="btn btn-outline-secondary text-start ${currentWorkerId === null ? 'active' : ''}"
|
|
onclick="updateWorker(${jobId}, null)">
|
|
<span class="text-muted">Unassigned</span>
|
|
${currentWorkerId === null ? '<i class="bi bi-check-circle-fill float-end"></i>' : ''}
|
|
</button>
|
|
`;
|
|
|
|
optionsHtml += workers.map(w => `
|
|
<button type="button"
|
|
class="btn btn-outline-info text-start ${w.id === currentWorkerId ? 'active' : ''}"
|
|
onclick="updateWorker(${jobId}, '${w.id}')">
|
|
<i class="bi bi-person me-2"></i>${w.name}
|
|
${w.id === currentWorkerId ? '<i class="bi bi-check-circle-fill float-end"></i>' : ''}
|
|
</button>
|
|
`).join('');
|
|
|
|
document.getElementById('workerOptions').innerHTML = optionsHtml;
|
|
workerModal.show();
|
|
}
|
|
|
|
function openScheduledDateModal(jobId, currentDate, jobNumber) {
|
|
currentJobId = jobId;
|
|
document.getElementById('scheduledDateJobNumber').textContent = jobNumber;
|
|
document.getElementById('scheduledDateInput').value = currentDate || '';
|
|
scheduledDateModal.show();
|
|
}
|
|
|
|
function openDueDateModal(jobId, currentDate, jobNumber) {
|
|
currentJobId = jobId;
|
|
document.getElementById('dueDateJobNumber').textContent = jobNumber;
|
|
document.getElementById('dueDateInput').value = currentDate || '';
|
|
dueDateModal.show();
|
|
}
|
|
|
|
function saveScheduledDate() {
|
|
const dateValue = document.getElementById('scheduledDateInput').value;
|
|
updateScheduledDate(currentJobId, dateValue);
|
|
}
|
|
|
|
function clearScheduledDate() {
|
|
updateScheduledDate(currentJobId, null);
|
|
}
|
|
|
|
function saveDueDate() {
|
|
const dateValue = document.getElementById('dueDateInput').value;
|
|
updateDueDate(currentJobId, dateValue);
|
|
}
|
|
|
|
function clearDueDate() {
|
|
updateDueDate(currentJobId, null);
|
|
}
|
|
|
|
function updateScheduledDate(jobId, scheduledDate) {
|
|
fetch('@Url.Action("UpdateScheduledDate", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: `jobId=${jobId}&scheduledDate=${scheduledDate || ''}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update the date display in the table
|
|
const displayElement = document.querySelector(`.scheduled-date-display-${jobId}`);
|
|
if (data.scheduledDate) {
|
|
displayElement.innerHTML = `
|
|
<span>
|
|
<i class="bi bi-calendar-check me-1"></i>
|
|
${data.scheduledDate}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
} else {
|
|
displayElement.innerHTML = `
|
|
<span class="text-muted">
|
|
Not set
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
scheduledDateModal.hide();
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update scheduled date', 'danger');
|
|
});
|
|
}
|
|
|
|
function updateDueDate(jobId, dueDate) {
|
|
fetch('@Url.Action("UpdateDueDate", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: `jobId=${jobId}&dueDate=${dueDate || ''}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update the date display in the table
|
|
const displayElement = document.querySelector(`.due-date-display-${jobId}`);
|
|
if (data.dueDate) {
|
|
const overdueClass = data.isOverdue ? 'text-danger fw-bold' : '';
|
|
const overdueIcon = data.isOverdue ? '<i class="bi bi-exclamation-triangle-fill ms-1"></i>' : '';
|
|
displayElement.innerHTML = `
|
|
<span class="${overdueClass}">
|
|
<i class="bi bi-calendar-x me-1"></i>
|
|
${data.dueDate}
|
|
${overdueIcon}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
} else {
|
|
displayElement.innerHTML = `
|
|
<span class="text-muted">
|
|
Not set
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
dueDateModal.hide();
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update due date', 'danger');
|
|
});
|
|
}
|
|
|
|
function updatePriority(jobId, priorityId) {
|
|
fetch('@Url.Action("UpdatePriority", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: `jobId=${jobId}&priorityId=${priorityId}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update the badge in the table
|
|
const badgeElement = document.querySelector(`.priority-badge-${jobId}`);
|
|
badgeElement.className = `badge bg-${data.colorClass} priority-badge-${jobId}`;
|
|
badgeElement.innerHTML = `${data.displayName} <i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>`;
|
|
|
|
priorityModal.hide();
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update priority', 'danger');
|
|
});
|
|
}
|
|
|
|
function updateWorker(jobId, workerId) {
|
|
fetch('@Url.Action("UpdateWorker", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: `jobId=${jobId}&workerId=${workerId || ''}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update the worker display in the table
|
|
const displayElement = document.querySelector(`.worker-display-${jobId}`);
|
|
if (data.workerName === 'Unassigned') {
|
|
displayElement.innerHTML = `
|
|
<span class="text-muted">
|
|
Unassigned
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
} else {
|
|
displayElement.innerHTML = `
|
|
<span class="badge bg-info">
|
|
<i class="bi bi-person me-1"></i>
|
|
${data.workerName}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
workerModal.hide();
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update worker assignment', 'danger');
|
|
});
|
|
}
|
|
|
|
let currentMaintenanceId = null;
|
|
let maintenanceWorkerModal;
|
|
|
|
function openMaintenanceWorkerModal(maintenanceId, currentWorkerId, equipmentName) {
|
|
currentMaintenanceId = maintenanceId;
|
|
document.getElementById('maintenanceWorkerEquipmentName').textContent = equipmentName;
|
|
|
|
let optionsHtml = `
|
|
<button type="button"
|
|
class="btn btn-outline-secondary text-start ${currentWorkerId === null ? 'active' : ''}"
|
|
onclick="updateMaintenanceWorker(${maintenanceId}, null)">
|
|
<span class="text-muted">Unassigned</span>
|
|
${currentWorkerId === null ? '<i class="bi bi-check-circle-fill float-end"></i>' : ''}
|
|
</button>
|
|
`;
|
|
|
|
optionsHtml += workers.map(w => `
|
|
<button type="button"
|
|
class="btn btn-outline-info text-start ${w.id === currentWorkerId ? 'active' : ''}"
|
|
onclick="updateMaintenanceWorker(${maintenanceId}, '${w.id}')">
|
|
<i class="bi bi-person me-2"></i>${w.name}
|
|
${w.id === currentWorkerId ? '<i class="bi bi-check-circle-fill float-end"></i>' : ''}
|
|
</button>
|
|
`).join('');
|
|
|
|
document.getElementById('maintenanceWorkerOptions').innerHTML = optionsHtml;
|
|
maintenanceWorkerModal.show();
|
|
}
|
|
|
|
function updateMaintenanceWorker(maintenanceId, workerId) {
|
|
fetch('@Url.Action("UpdateMaintenanceWorker", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: `maintenanceId=${maintenanceId}&workerId=${workerId || ''}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const displayElement = document.querySelector(`.maint-worker-display-${maintenanceId}`);
|
|
if (data.workerName === 'Unassigned') {
|
|
displayElement.innerHTML = `
|
|
<span class="text-muted">
|
|
Unassigned
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
} else {
|
|
displayElement.innerHTML = `
|
|
<span class="badge bg-info text-dark">
|
|
<i class="bi bi-person me-1"></i>
|
|
${data.workerName}
|
|
<i class="bi bi-pencil-fill ms-1" style="font-size: 0.75rem;"></i>
|
|
</span>
|
|
`;
|
|
}
|
|
maintenanceWorkerModal.hide();
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update worker assignment', 'danger');
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
priorityModal = new bootstrap.Modal(document.getElementById('priorityModal'));
|
|
workerModal = new bootstrap.Modal(document.getElementById('workerModal'));
|
|
maintenanceWorkerModal = new bootstrap.Modal(document.getElementById('maintenanceWorkerModal'));
|
|
scheduledDateModal = new bootstrap.Modal(document.getElementById('scheduledDateModal'));
|
|
dueDateModal = new bootstrap.Modal(document.getElementById('dueDateModal'));
|
|
const sortableElement = document.getElementById('sortableJobs');
|
|
|
|
if (sortableElement) {
|
|
const sortable = new Sortable(sortableElement, {
|
|
handle: '.drag-handle',
|
|
animation: 150,
|
|
ghostClass: 'sortable-ghost',
|
|
dragClass: 'sortable-drag',
|
|
onEnd: function(evt) {
|
|
saveOrder();
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveOrder() {
|
|
const rows = document.querySelectorAll('.sortable-row');
|
|
const updates = [];
|
|
|
|
rows.forEach((row, index) => {
|
|
updates.push({
|
|
jobId: parseInt(row.getAttribute('data-job-id')),
|
|
displayOrder: index + 1
|
|
});
|
|
});
|
|
|
|
fetch('@Url.Action("UpdateOrder", "JobsPriority")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: JSON.stringify(updates)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast('Success', data.message, 'success');
|
|
} else {
|
|
showToast('Error', data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showToast('Error', 'Failed to update job order', 'danger');
|
|
});
|
|
}
|
|
|
|
function showToast(title, message, type) {
|
|
// Simple toast notification (you can use Bootstrap Toast or custom implementation)
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`;
|
|
alertDiv.style.zIndex = '9999';
|
|
alertDiv.innerHTML = `
|
|
<strong>${title}:</strong> ${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
alertDiv.remove();
|
|
}, 3000);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
// SignalR — real-time status updates from shop display
|
|
(function () {
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl('/hubs/shop')
|
|
.withAutomaticReconnect()
|
|
.build();
|
|
|
|
// A worker advanced a job status on the shop display — update badge in place
|
|
connection.on('JobStatusChanged', function (data) {
|
|
const badge = document.getElementById(`status-badge-${data.jobId}`);
|
|
if (badge) {
|
|
badge.className = `badge bg-${data.statusColorClass}`;
|
|
badge.textContent = data.statusDisplayName;
|
|
}
|
|
});
|
|
|
|
// Order/priority/worker changed by another Daily Board session — reload
|
|
connection.on('DailyBoardUpdated', function () {
|
|
// Only reload if this tab didn't trigger it (we can't easily tell, so just reload)
|
|
location.reload();
|
|
});
|
|
|
|
connection.start().catch(err => console.warn('ShopHub connect failed:', err));
|
|
})();
|
|
</script>
|
|
|
|
<style>
|
|
.sortable-ghost {
|
|
opacity: 0.4;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.sortable-drag {
|
|
opacity: 0;
|
|
}
|
|
|
|
.drag-handle {
|
|
cursor: grab;
|
|
}
|
|
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.sortable-row .clickable-cell {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sortable-row .clickable-cell:hover {
|
|
background-color: rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
[data-bs-theme="dark"] .sortable-row .clickable-cell:hover {
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.sortable-row .editable-cell:hover {
|
|
background-color: rgba(13, 110, 253, 0.05);
|
|
}
|
|
|
|
[data-bs-theme="dark"] .sortable-row .editable-cell:hover {
|
|
background-color: rgba(13, 110, 253, 0.15);
|
|
}
|
|
|
|
tr.sortable-row {
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
#priorityOptions .btn,
|
|
#workerOptions .btn {
|
|
position: relative;
|
|
}
|
|
|
|
#priorityOptions .btn.active,
|
|
#workerOptions .btn.active {
|
|
border-width: 2px;
|
|
}
|
|
</style>
|
|
|
|
@Html.AntiForgeryToken()
|
|
}
|