Add carried-over jobs section to Daily Board and fix tip visibility
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>
This commit is contained in:
@@ -85,4 +85,11 @@ public interface IJobRepository : IRepository<Job>
|
||||
/// Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForTemplateSnapshotAsync(int jobId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-terminal jobs whose <c>ScheduledDate</c> is before today and not null,
|
||||
/// ordered by scheduled date then job number. Used by the Daily Board to surface jobs that
|
||||
/// were never completed and rolled past their scheduled day.
|
||||
/// </summary>
|
||||
Task<List<Job>> GetOverdueScheduledJobsAsync();
|
||||
}
|
||||
|
||||
@@ -187,4 +187,22 @@ public class JobRepository : Repository<Job>, IJobRepository
|
||||
.ThenInclude(i => i.PrepServices.Where(p => !p.IsDeleted))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Job>> GetOverdueScheduledJobsAsync()
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
return await _context.Jobs
|
||||
.Include(j => j.Customer)
|
||||
.Include(j => j.JobStatus)
|
||||
.Include(j => j.JobPriority)
|
||||
.Include(j => j.AssignedUser)
|
||||
.Where(j => j.ScheduledDate.HasValue
|
||||
&& j.ScheduledDate.Value.Date < today
|
||||
&& !j.IsDeleted
|
||||
&& !j.JobStatus.IsTerminalStatus)
|
||||
.OrderBy(j => j.ScheduledDate)
|
||||
.ThenBy(j => j.JobNumber)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,29 @@ public class JobsPriorityController : Controller
|
||||
.ThenBy(m => m.ScheduledDate)
|
||||
.ToList();
|
||||
|
||||
// Load overdue jobs only when viewing today — past-date navigation shows that day as-is
|
||||
if (today == DateTime.Today)
|
||||
{
|
||||
var overdueJobs = await _unitOfWork.Jobs.GetOverdueScheduledJobsAsync();
|
||||
ViewBag.OverdueJobs = overdueJobs.Select(j => new JobDailyPriorityDto
|
||||
{
|
||||
Id = 0,
|
||||
JobId = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
CustomerName = j.Customer.CompanyName ?? $"{j.Customer.ContactFirstName} {j.Customer.ContactLastName}".Trim(),
|
||||
StatusDisplayName = j.JobStatus.DisplayName,
|
||||
StatusColorClass = j.JobStatus.ColorClass,
|
||||
JobPriorityId = j.JobPriorityId,
|
||||
PriorityDisplayName = j.JobPriority.DisplayName,
|
||||
PriorityColorClass = j.JobPriority.ColorClass,
|
||||
AssignedUserId = j.AssignedUserId,
|
||||
AssignedWorkerName = j.AssignedUser?.FullName,
|
||||
ScheduledDate = j.ScheduledDate,
|
||||
DueDate = j.DueDate,
|
||||
DisplayOrder = int.MaxValue
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
ViewBag.ScheduledDate = today;
|
||||
ViewBag.MaintenanceItems = maintenanceItems;
|
||||
ViewBag.PrioritiesJson = priorities.OrderBy(p => p.DisplayOrder)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
|
||||
@using PowderCoating.Application.DTOs.Job
|
||||
@using PowderCoating.Core.Entities
|
||||
@using PowderCoating.Core.Enums
|
||||
|
||||
@@ -9,6 +10,7 @@
|
||||
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">
|
||||
@@ -49,6 +51,123 @@
|
||||
</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">
|
||||
@@ -194,7 +313,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info border-info shadow-sm" style="position: sticky; bottom: 0; z-index: 1020; margin-bottom: 0;">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user