Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,894 @@
@model IEnumerable<PowderCoating.Application.DTOs.Job.JobDailyPriorityDto>
@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>();
}
<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>
@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 &amp; 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 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()
}