666 lines
26 KiB
Plaintext
666 lines
26 KiB
Plaintext
@model List<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
|
|
@using PowderCoating.Core.Entities
|
|
|
|
@{
|
|
Layout = null;
|
|
ViewData["Title"] = "Shop Floor Display";
|
|
var scheduledDate = ViewBag.ScheduledDate as DateTime? ?? DateTime.Today;
|
|
var currentTime = ViewBag.CurrentTime as DateTime? ?? DateTime.Now;
|
|
var allStatuses = ViewBag.AllStatuses as List<JobStatusLookup> ?? new List<JobStatusLookup>();
|
|
var currentUserId = ViewBag.CurrentUserId as string ?? "";
|
|
var filterUserId = ViewBag.FilterUserId as string;
|
|
var isMyJobsFilter = filterUserId != null;
|
|
}
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Shop Floor Display - @scheduledDate.ToString("MMM dd, yyyy")</title>
|
|
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="~/lib/bootstrap-icons/font/bootstrap-icons.css">
|
|
<style>
|
|
body {
|
|
background-color: #0d1117;
|
|
color: #e0e0e0;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.shop-header {
|
|
background: linear-gradient(135deg, #161b22 0%, #1c2128 100%);
|
|
border-bottom: 3px solid #30363d;
|
|
padding: 20px 40px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.shop-title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.shop-subtitle {
|
|
font-size: 1rem;
|
|
color: #8b949e;
|
|
margin-top: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.clock {
|
|
font-size: 2.8rem;
|
|
font-weight: 300;
|
|
color: #58a6ff;
|
|
font-family: 'Courier New', monospace;
|
|
letter-spacing: 4px;
|
|
}
|
|
|
|
.date-badge {
|
|
font-size: 1rem;
|
|
color: #8b949e;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.job-count {
|
|
background: #21262d;
|
|
border: 1px solid #30363d;
|
|
border-radius: 20px;
|
|
padding: 4px 14px;
|
|
font-size: 1rem;
|
|
color: #58a6ff;
|
|
}
|
|
|
|
.nav-buttons { display: flex; gap: 10px; }
|
|
|
|
.nav-btn {
|
|
border: 1px solid #30363d;
|
|
color: #8b949e;
|
|
font-size: 0.95rem;
|
|
padding: 8px 16px;
|
|
background: #161b22;
|
|
}
|
|
|
|
.nav-btn:hover { background-color: #21262d; border-color: #58a6ff; color: #58a6ff; }
|
|
.nav-btn.today { border-color: #58a6ff; color: #58a6ff; }
|
|
.nav-btn.my-jobs-active { border-color: #3fb950; color: #3fb950; background: rgba(63,185,80,0.1); }
|
|
|
|
/* Job card */
|
|
.job-card {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-left: 6px solid #30363d;
|
|
border-radius: 12px;
|
|
margin-bottom: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.job-card:hover { transform: translateX(4px); box-shadow: 0 8px 24px rgba(0,0,0,0.4); border-color: #58a6ff44; }
|
|
.job-card.no-advance { cursor: default; }
|
|
.job-card.no-advance:hover { transform: none; box-shadow: none; border-color: #30363d; }
|
|
.job-card.advancing { opacity: 0.6; pointer-events: none; }
|
|
|
|
.priority-border-danger { border-left-color: #dc3545 !important; }
|
|
.priority-border-warning { border-left-color: #ffc107 !important; }
|
|
.priority-border-info { border-left-color: #0dcaf0 !important; }
|
|
.priority-border-primary { border-left-color: #0d6efd !important; }
|
|
.priority-border-secondary { border-left-color: #6c757d !important; }
|
|
.priority-border-success { border-left-color: #198754 !important; }
|
|
|
|
/* Card top row */
|
|
.card-top { padding: 18px 28px 12px 28px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
|
|
|
|
.order-num {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
color: rgba(88,166,255,0.18);
|
|
min-width: 48px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.job-number { font-size: 1.8rem; font-weight: 700; color: #58a6ff; letter-spacing: 2px; min-width: 190px; }
|
|
.customer-name { font-size: 1.4rem; font-weight: 600; color: #f0f6fc; flex: 1; }
|
|
|
|
.badge-xl {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
min-width: 130px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* bg-info is a light cyan — use dark text so it's readable on TV */
|
|
.badge-xl.bg-info { color: #000 !important; }
|
|
|
|
.worker-badge {
|
|
font-size: 1rem;
|
|
padding: 8px 16px;
|
|
background: rgba(63,185,80,0.15);
|
|
color: #3fb950;
|
|
border: 1px solid rgba(63,185,80,0.3);
|
|
border-radius: 8px;
|
|
min-width: 160px;
|
|
text-align: center;
|
|
}
|
|
|
|
.worker-badge.unassigned {
|
|
background: rgba(139,148,158,0.15);
|
|
color: #8b949e;
|
|
border-color: rgba(139,148,158,0.3);
|
|
}
|
|
|
|
.due-date { font-size: 1rem; color: #8b949e; min-width: 150px; text-align: right; }
|
|
.due-date.overdue { color: #f85149; font-weight: 700; }
|
|
|
|
/* Card detail row */
|
|
.card-detail {
|
|
padding: 0 28px 14px 88px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
border-top: 1px solid #21262d;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.detail-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: #21262d;
|
|
border: 1px solid #30363d;
|
|
border-radius: 20px;
|
|
padding: 4px 12px;
|
|
font-size: 0.9rem;
|
|
color: #8b949e;
|
|
}
|
|
|
|
.prep-flag { background: rgba(255,193,7,0.1); border-color: rgba(255,193,7,0.3); color: #ffc107; }
|
|
.desc-text {
|
|
color: #c9d1d9;
|
|
font-size: 0.95rem;
|
|
flex: 1;
|
|
min-width: 200px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 400px;
|
|
}
|
|
|
|
/* Per-item list in card detail */
|
|
.item-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
}
|
|
|
|
.item-entry {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 6px;
|
|
font-size: 0.9rem;
|
|
color: #c9d1d9;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.item-qty {
|
|
font-weight: 700;
|
|
color: #58a6ff;
|
|
min-width: 30px;
|
|
text-align: right;
|
|
font-size: 0.85rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.item-desc {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.item-sep { color: #484f58; flex-shrink: 0; }
|
|
|
|
.item-color {
|
|
color: #a371f7;
|
|
font-style: italic;
|
|
}
|
|
|
|
.item-flag {
|
|
font-size: 0.75rem;
|
|
color: #ffc107;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.special-instructions {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
color: #ffc107;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
background: rgba(255,193,7,0.08);
|
|
border: 1px solid rgba(255,193,7,0.25);
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Progress strip */
|
|
.progress-strip {
|
|
padding: 8px 28px 14px 88px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.progress-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
font-size: 0.72rem;
|
|
color: #484f58;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.step-label {
|
|
padding: 3px 10px;
|
|
border-radius: 10px;
|
|
background: #21262d;
|
|
border: 1px solid #30363d;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.step-label.current {
|
|
background: #1f6feb;
|
|
border-color: #58a6ff;
|
|
color: #ffffff;
|
|
font-weight: 600;
|
|
font-size: 0.78rem;
|
|
box-shadow: 0 0 10px rgba(88,166,255,0.4);
|
|
}
|
|
|
|
.step-label.done { background: #1a2d1a; border-color: #238636; color: #3fb950; }
|
|
.step-arrow { color: #30363d; margin: 0 4px; font-size: 0.65rem; }
|
|
.step-arrow.done { color: #238636; }
|
|
|
|
/* Advance button hint */
|
|
.advance-hint { font-size: 0.75rem; color: #3d444d; padding: 0 28px 10px 88px; }
|
|
.job-card:hover .advance-hint { color: #58a6ff99; }
|
|
|
|
/* No-jobs state */
|
|
.no-jobs-container {
|
|
min-height: 70vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #8b949e;
|
|
}
|
|
|
|
.no-jobs-icon { font-size: 8rem; color: #3fb950; opacity: 0.6; }
|
|
|
|
/* Modal overrides for dark theme */
|
|
.modal-content { background: #161b22; border: 1px solid #30363d; color: #e0e0e0; }
|
|
.modal-header { border-bottom: 1px solid #30363d; }
|
|
.modal-footer { border-top: 1px solid #30363d; }
|
|
.btn-close { filter: invert(1); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Header -->
|
|
<div class="shop-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="shop-title"><i class="bi bi-calendar-week me-3"></i>Job Schedule</div>
|
|
<div class="shop-subtitle">
|
|
<span class="job-count"><i class="bi bi-briefcase me-2"></i>@Model.Count job@(Model.Count != 1 ? "s" : "")</span>
|
|
<span class="text-muted" style="font-size:0.85rem"><i class="bi bi-wifi me-1"></i><span id="shopHubStatus">Connecting...</span></span>
|
|
@if (isMyJobsFilter)
|
|
{
|
|
<span style="color:#3fb950;font-size:0.85rem"><i class="bi bi-person-check me-1"></i>My Jobs</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="clock" id="clock">@currentTime.ToString("HH:mm:ss")</div>
|
|
<div class="date-badge">
|
|
@scheduledDate.ToString("dddd, 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>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="nav-buttons">
|
|
<a href="@Url.Action("ShopDisplay", new { date = scheduledDate.AddDays(-1), userId = filterUserId })" class="btn nav-btn">
|
|
<i class="bi bi-chevron-left"></i>
|
|
</a>
|
|
<a href="@Url.Action("ShopDisplay", new { userId = filterUserId })" class="btn nav-btn @(scheduledDate.Date == DateTime.Today ? "today" : "")">TODAY</a>
|
|
<a href="@Url.Action("ShopDisplay", new { date = scheduledDate.AddDays(1), userId = filterUserId })" class="btn nav-btn">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</a>
|
|
@if (isMyJobsFilter)
|
|
{
|
|
<a href="@Url.Action("ShopDisplay", new { date = scheduledDate })" class="btn nav-btn my-jobs-active">
|
|
<i class="bi bi-people me-1"></i>All Jobs
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<a href="@Url.Action("ShopDisplay", new { date = scheduledDate, userId = currentUserId })" class="btn nav-btn">
|
|
<i class="bi bi-person me-1"></i>My Jobs
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Anti-forgery token for AJAX -->
|
|
@Html.AntiForgeryToken()
|
|
|
|
<!-- Main Content -->
|
|
<div class="container-fluid p-4">
|
|
@if (!Model.Any())
|
|
{
|
|
<div class="no-jobs-container">
|
|
<i class="bi bi-calendar-x no-jobs-icon"></i>
|
|
<h2 class="mt-4" style="color:#8b949e;font-size:2.5rem;">
|
|
@(isMyJobsFilter ? "No Jobs Assigned to You" : "No Jobs Scheduled")
|
|
</h2>
|
|
<p class="mt-3" style="font-size:1.3rem;">
|
|
@(isMyJobsFilter ? "No jobs assigned to you for " : "No jobs scheduled for ")@scheduledDate.ToString("MMMM dd, yyyy")
|
|
</p>
|
|
@if (isMyJobsFilter)
|
|
{
|
|
<a href="@Url.Action("ShopDisplay", new { date = scheduledDate })" class="btn btn-outline-secondary mt-3">
|
|
<i class="bi bi-people me-2"></i>View All Jobs
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@for (int i = 0; i < Model.Count; i++)
|
|
{
|
|
var job = Model[i];
|
|
var isOverdue = job.DueDate.HasValue && job.DueDate.Value.Date < DateTime.Today;
|
|
var borderClass = $"priority-border-{job.PriorityColorClass}";
|
|
var hasNext = job.NextStatusId.HasValue;
|
|
|
|
<div class="job-card @borderClass @(hasNext ? "" : "no-advance")" id="job-card-@job.JobId"
|
|
data-job-id="@job.JobId"
|
|
data-job-number="@job.JobNumber"
|
|
data-next-status-id="@job.NextStatusId"
|
|
data-next-status-name="@job.NextStatusDisplayName"
|
|
data-next-status-color="@job.NextStatusColorClass">
|
|
|
|
<!-- Top Row: order number, job number, customer, status, priority, worker, due date -->
|
|
<div class="card-top">
|
|
<div class="order-num">@(i + 1)</div>
|
|
<div class="job-number">@job.JobNumber</div>
|
|
<div class="customer-name"><i class="bi bi-building me-2" style="color:#8b949e"></i>@job.CustomerName</div>
|
|
<div>
|
|
<span class="badge bg-@job.StatusColorClass badge-xl" id="status-badge-@job.JobId">@job.StatusDisplayName</span>
|
|
</div>
|
|
<div>
|
|
<span class="badge bg-@job.PriorityColorClass badge-xl">@job.PriorityDisplayName</span>
|
|
</div>
|
|
<div>
|
|
<div class="worker-badge @(string.IsNullOrEmpty(job.AssignedWorkerName) ? "unassigned" : "")">
|
|
<i class="bi bi-person-badge me-2"></i>@(string.IsNullOrEmpty(job.AssignedWorkerName) ? "Unassigned" : job.AssignedWorkerName)
|
|
</div>
|
|
</div>
|
|
@if (job.DueDate.HasValue)
|
|
{
|
|
<div class="due-date @(isOverdue ? "overdue" : "")">
|
|
<i class="bi bi-calendar-x me-2"></i>Due: @job.DueDate.Value.ToString("MMM dd")
|
|
@if (isOverdue) { <i class="bi bi-exclamation-triangle-fill ms-2"></i> }
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="due-date" style="color:#30363d">
|
|
<i class="bi bi-calendar me-2"></i>No due date
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Detail Row: items with colors, prep flags -->
|
|
<div class="card-detail">
|
|
<div class="item-list">
|
|
@foreach (var item in job.Items)
|
|
{
|
|
<span class="item-entry">
|
|
<span class="item-qty">×@((int)item.Quantity)</span>
|
|
<span class="item-desc">@item.Description</span>
|
|
@if (item.Colors.Any())
|
|
{
|
|
<span class="item-sep">—</span>
|
|
<i class="bi bi-circle-fill" style="color:#a371f7;font-size:0.55rem;flex-shrink:0"></i>
|
|
<span class="item-color">@string.Join(" / ", item.Colors)</span>
|
|
}
|
|
@if (item.HasSandblasting)
|
|
{
|
|
<i class="bi bi-tornado item-flag" title="Sandblast"></i>
|
|
}
|
|
@if (item.HasMasking)
|
|
{
|
|
<i class="bi bi-mask item-flag" title="Masking"></i>
|
|
}
|
|
</span>
|
|
}
|
|
</div>
|
|
@if (!string.IsNullOrWhiteSpace(job.SpecialInstructions))
|
|
{
|
|
<div class="special-instructions">
|
|
<i class="bi bi-exclamation-circle-fill me-2" style="color:#ffc107;flex-shrink:0"></i>@job.SpecialInstructions
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Progress Strip -->
|
|
@if (allStatuses.Any())
|
|
{
|
|
<div class="progress-strip">
|
|
@for (int s = 0; s < allStatuses.Count; s++)
|
|
{
|
|
var status = allStatuses[s];
|
|
var isCurrent = status.StatusCode == job.StatusCode;
|
|
var isDone = status.DisplayOrder < job.StatusDisplayOrder;
|
|
var stepClass = isCurrent ? "current" : (isDone ? "done" : "");
|
|
<div class="progress-step">
|
|
<span class="step-label @stepClass">@status.DisplayName</span>
|
|
@if (s < allStatuses.Count - 1)
|
|
{
|
|
<span class="step-arrow @(isDone ? "done" : "")"><i class="bi bi-chevron-right"></i></span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Advance hint -->
|
|
@if (hasNext)
|
|
{
|
|
<div class="advance-hint">
|
|
<i class="bi bi-hand-index me-1"></i>Tap to advance to <strong>@job.NextStatusDisplayName</strong>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
<!-- Advance Status Modal -->
|
|
<div class="modal fade" id="advanceModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-arrow-right-circle me-2 text-primary"></i>Advance Job Status</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body py-4">
|
|
<p class="fs-5 mb-1">Move <strong id="modal-job-number" class="text-primary"></strong> to:</p>
|
|
<p class="fs-4 fw-bold mt-2" id="modal-next-status"></p>
|
|
<p class="text-muted small mb-0">This will update the job status immediately.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="btn-confirm-advance">
|
|
<i class="bi bi-check-lg me-2"></i>Yes, Advance
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
|
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
|
|
<script>
|
|
// Live clock
|
|
function updateClock() {
|
|
const now = new Date();
|
|
document.getElementById('clock').textContent =
|
|
String(now.getHours()).padStart(2, '0') + ':' +
|
|
String(now.getMinutes()).padStart(2, '0') + ':' +
|
|
String(now.getSeconds()).padStart(2, '0');
|
|
}
|
|
updateClock();
|
|
setInterval(updateClock, 1000);
|
|
|
|
// Advance status modal
|
|
let pendingJobId = null;
|
|
let pendingStatusId = null;
|
|
const advanceModal = new bootstrap.Modal(document.getElementById('advanceModal'));
|
|
|
|
function openAdvanceModal(jobId, jobNumber, nextStatusId, nextStatusName, nextStatusColor) {
|
|
pendingJobId = jobId;
|
|
pendingStatusId = nextStatusId;
|
|
document.getElementById('modal-job-number').textContent = jobNumber;
|
|
document.getElementById('modal-next-status').innerHTML =
|
|
`<span class="badge bg-${nextStatusColor} fs-5 px-3 py-2">${nextStatusName}</span>`;
|
|
advanceModal.show();
|
|
}
|
|
|
|
// Use event delegation so Razor encoding doesn't break onclick attributes
|
|
document.addEventListener('click', function (e) {
|
|
const card = e.target.closest('.job-card:not(.no-advance)');
|
|
if (!card) return;
|
|
const nextStatusId = card.dataset.nextStatusId;
|
|
if (!nextStatusId) return;
|
|
openAdvanceModal(
|
|
parseInt(card.dataset.jobId),
|
|
card.dataset.jobNumber,
|
|
parseInt(nextStatusId),
|
|
card.dataset.nextStatusName,
|
|
card.dataset.nextStatusColor
|
|
);
|
|
});
|
|
|
|
document.getElementById('btn-confirm-advance').addEventListener('click', async function () {
|
|
if (!pendingJobId || !pendingStatusId) return;
|
|
|
|
const btn = this;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
|
|
|
|
const card = document.getElementById('job-card-' + pendingJobId);
|
|
if (card) card.classList.add('advancing');
|
|
|
|
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
|
|
|
try {
|
|
const resp = await fetch('@Url.Action("AdvanceJobStatus", "Jobs")', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'RequestVerificationToken': token
|
|
},
|
|
body: JSON.stringify({ jobId: pendingJobId, newStatusId: pendingStatusId })
|
|
});
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
advanceModal.hide();
|
|
// Update badge in place
|
|
const badge = document.getElementById('status-badge-' + pendingJobId);
|
|
if (badge) {
|
|
badge.className = `badge bg-${result.newStatusColorClass} badge-xl`;
|
|
badge.textContent = result.newStatusDisplayName;
|
|
}
|
|
// Reload page after short delay so progress strip refreshes
|
|
setTimeout(() => location.reload(), 1200);
|
|
} else {
|
|
alert('Error: ' + (result.message || 'Could not advance status'));
|
|
if (card) card.classList.remove('advancing');
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Yes, Advance';
|
|
}
|
|
} catch (err) {
|
|
alert('Network error. Please try again.');
|
|
if (card) card.classList.remove('advancing');
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Yes, Advance';
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
// SignalR — real-time updates
|
|
(function () {
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl('/hubs/shop')
|
|
.withAutomaticReconnect()
|
|
.build();
|
|
|
|
const statusEl = document.getElementById('shopHubStatus');
|
|
|
|
function setStatus(text, color) {
|
|
if (statusEl) { statusEl.textContent = text; statusEl.style.color = color || ''; }
|
|
}
|
|
|
|
connection.onreconnecting(() => setStatus('Reconnecting...', '#fd7e14'));
|
|
connection.onreconnected(() => setStatus('Live', '#198754'));
|
|
connection.onclose(() => setStatus('Disconnected', '#dc3545'));
|
|
|
|
// Daily Board changed order/priority/worker — reload job list
|
|
connection.on('DailyBoardUpdated', function () {
|
|
const url = new URL(window.location.href);
|
|
fetch(url.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
.then(() => location.reload())
|
|
.catch(() => {});
|
|
});
|
|
|
|
// A job status was advanced — reload so progress strip updates
|
|
connection.on('JobStatusChanged', function () {
|
|
location.reload();
|
|
});
|
|
|
|
connection.start()
|
|
.then(() => setStatus('Live', '#198754'))
|
|
.catch(() => setStatus('Offline — refresh manually', '#dc3545'));
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|