Initial commit
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
@using PowderCoating.Web.Controllers
|
||||
@model List<JobBoardColumn>
|
||||
@{
|
||||
ViewData["Title"] = "Jobs Board";
|
||||
bool showTerminal = ViewBag.ShowTerminal == true;
|
||||
int totalTerminal = (int)(ViewBag.TotalTerminal ?? 0);
|
||||
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.board-outer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 130px);
|
||||
min-height: 500px;
|
||||
}
|
||||
.board-toolbar { flex-shrink: 0; padding-bottom: .75rem; }
|
||||
.board-scroll {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
padding-bottom: .5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Column */
|
||||
.board-column {
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
background: var(--pcl-paper-2);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--pcl-rule);
|
||||
overflow: hidden;
|
||||
}
|
||||
.board-column-header {
|
||||
padding: .55rem .75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--pcl-rule);
|
||||
background: var(--pcl-card);
|
||||
}
|
||||
.board-column-header-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--pcl-steel);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.col-count {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
background: var(--pcl-rule);
|
||||
color: var(--pcl-steel);
|
||||
border-radius: 99px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
.board-column-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: .5rem .5rem .75rem;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Oven meter strip (Curing column only) */
|
||||
.board-oven-meter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 12px;
|
||||
background: var(--pcl-ember-tint);
|
||||
border-bottom: 1px solid var(--pcl-rule);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--pcl-ember-ink);
|
||||
}
|
||||
.board-oven-dot {
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--pcl-ember);
|
||||
flex-shrink: 0;
|
||||
animation: pcl-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.board-card {
|
||||
background: var(--pcl-card);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--pcl-rule);
|
||||
margin-bottom: .5rem;
|
||||
padding: .6rem .7rem;
|
||||
cursor: grab;
|
||||
transition: background 0.1s;
|
||||
text-decoration: none;
|
||||
color: var(--pcl-ink);
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.board-card:hover { background: var(--pcl-paper-2); color: var(--pcl-ink); }
|
||||
.board-card.board-card-hot { box-shadow: inset 2px 0 0 var(--pcl-bad); }
|
||||
.board-card.dragging { opacity: .5; cursor: grabbing; }
|
||||
|
||||
/* Card content */
|
||||
.card-job-number { font-family: var(--font-mono); font-weight: 500; font-size: .8rem; color: var(--pcl-ink); }
|
||||
.card-customer { font-size: .75rem; color: var(--pcl-steel); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card-desc { font-size: .72rem; color: var(--pcl-steel); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: .15rem; }
|
||||
.card-footer-row {
|
||||
display: flex; align-items: center; gap: .4rem; margin-top: .4rem;
|
||||
flex-wrap: wrap; padding-top: .35rem;
|
||||
border-top: 1px solid var(--pcl-rule-soft);
|
||||
}
|
||||
|
||||
/* Priority dot in card footer */
|
||||
.board-priority-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.board-priority-danger { background: var(--pcl-bad); }
|
||||
.board-priority-warning { background: var(--pcl-warn); }
|
||||
.board-priority-success { background: var(--pcl-ok); }
|
||||
.board-priority-info { background: var(--pcl-cool); }
|
||||
.board-priority-dark, .board-priority-secondary { background: var(--pcl-mute); }
|
||||
|
||||
/* Drag / ghost */
|
||||
.board-column-body.drag-over { background: var(--pcl-ember-tint); border-radius: var(--radius); }
|
||||
.sortable-ghost {
|
||||
opacity: .3;
|
||||
background: var(--pcl-ember-tint);
|
||||
border: 2px dashed var(--pcl-ember);
|
||||
}
|
||||
.card-saving { pointer-events: none; }
|
||||
.card-saving::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: var(--pcl-card); opacity: .6; border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Hidden / dropdown */
|
||||
.board-column.col-hidden { display: none; }
|
||||
.col-visibility-menu { min-width: 200px; padding: .5rem 0; }
|
||||
.col-visibility-menu .form-check { padding: .3rem 1rem; margin: 0; cursor: pointer; }
|
||||
.col-visibility-menu .form-check:hover { background: var(--pcl-paper-2); }
|
||||
.col-visibility-menu .form-check-label { cursor: pointer; width: 100%; }
|
||||
.col-color-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .4rem; flex-shrink: 0; }
|
||||
|
||||
/* Column drag handle */
|
||||
.col-drag-handle { cursor: grab; opacity: 0; transition: opacity .15s; flex-shrink: 0; line-height: 1; }
|
||||
.board-column-header:hover .col-drag-handle { opacity: .5; }
|
||||
.col-drag-handle:hover { opacity: 1 !important; }
|
||||
.col-sortable-ghost { opacity: .35; outline: 2px dashed var(--pcl-ember); border-radius: var(--radius-lg); }
|
||||
|
||||
/* Segmented view switch */
|
||||
.board-view-switch { display: flex; border: 1px solid var(--pcl-rule); border-radius: var(--radius); overflow: hidden; }
|
||||
.board-view-switch a {
|
||||
padding: 4px 12px; font-size: 13px; font-weight: 500;
|
||||
text-decoration: none; color: var(--pcl-steel);
|
||||
border-right: 1px solid var(--pcl-rule);
|
||||
background: var(--pcl-card);
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
.board-view-switch a:last-child { border-right: none; }
|
||||
.board-view-switch a:hover { background: var(--pcl-paper-2); color: var(--pcl-ink); }
|
||||
.board-view-switch a.active { background: var(--pcl-ink); color: var(--pcl-paper); }
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="board-outer">
|
||||
@* Toolbar *@
|
||||
@{
|
||||
var _totalOnFloor = Model.Sum(c => c.Jobs.Count);
|
||||
var _hotCount = Model.Sum(c => c.Jobs.Count(j => j.IsOverdue));
|
||||
}
|
||||
<div class="board-toolbar">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
|
||||
@* Left: view switch + live stats *@
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="board-view-switch">
|
||||
<a asp-action="Board" class="active">Board</a>
|
||||
<a asp-action="Index">List</a>
|
||||
</div>
|
||||
<span class="mono" style="font-size:.75rem;color:var(--pcl-steel)">
|
||||
@_totalOnFloor job@(_totalOnFloor == 1 ? "" : "s") on floor
|
||||
@if (_hotCount > 0)
|
||||
{
|
||||
<span style="color:var(--pcl-bad)"> · @_hotCount overdue</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@* Right: actions *@
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a href="@Url.Action("Board", new { showTerminal = !showTerminal })"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
id="toggleTerminalBtn">
|
||||
<i class="bi bi-archive me-1"></i>
|
||||
@if (showTerminal) { <text>Hide Completed</text> }
|
||||
else if (totalTerminal > 0) { <text>Completed (@totalTerminal)</text> }
|
||||
else { <text>Completed</text> }
|
||||
</a>
|
||||
|
||||
@* Column visibility dropdown *@
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||
id="colVisBtn" data-bs-toggle="dropdown" data-bs-auto-close="outside"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-layout-three-columns me-1"></i>Columns
|
||||
<span class="badge bg-secondary ms-1" id="hiddenColCount" style="display:none"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu col-visibility-menu shadow" aria-labelledby="colVisBtn">
|
||||
<li class="px-3 pb-1 pt-2">
|
||||
<small class="text-muted fw-semibold text-uppercase" style="font-size:.7rem;letter-spacing:.04em">Show / Hide Columns</small>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
@foreach (var col in Model)
|
||||
{
|
||||
<li>
|
||||
<div class="form-check col-vis-item">
|
||||
<input class="form-check-input col-vis-check" type="checkbox"
|
||||
id="vis-@col.StatusId"
|
||||
data-status-id="@col.StatusId"
|
||||
checked />
|
||||
<label class="form-check-label d-flex align-items-center" for="vis-@col.StatusId">
|
||||
<span class="col-color-dot" style="background:var(--pcl-steel)"></span>
|
||||
@col.DisplayName
|
||||
<span class="ms-auto text-muted ps-2 mono" style="font-size:.72rem">@col.Jobs.Count</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<li class="px-3 pb-1">
|
||||
<button type="button" class="btn btn-xs btn-link p-0 text-muted small" id="showAllCols">
|
||||
Show all
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a asp-action="Create" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>New Job
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Board columns *@
|
||||
<div class="board-scroll" id="boardScroll">
|
||||
@foreach (var col in Model)
|
||||
{
|
||||
var isOvenPhase = col.DisplayName is "In Oven" or "Coating" or "Curing";
|
||||
|
||||
<div class="board-column"
|
||||
data-status-id="@col.StatusId"
|
||||
data-status-name="@col.DisplayName">
|
||||
|
||||
<div class="board-column-header">
|
||||
<span class="col-drag-handle" title="Drag to reorder column">
|
||||
<i class="bi bi-grip-vertical"></i>
|
||||
</span>
|
||||
<span class="board-column-header-label">@col.DisplayName</span>
|
||||
<span class="col-count">@col.Jobs.Count</span>
|
||||
</div>
|
||||
|
||||
@if (isOvenPhase && col.Jobs.Count > 0)
|
||||
{
|
||||
<div class="board-oven-meter">
|
||||
<span class="board-oven-dot"></span>
|
||||
<span>@col.Jobs.Count ACTIVE</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="board-column-body" id="col-@col.StatusId">
|
||||
@foreach (var card in col.Jobs)
|
||||
{
|
||||
var priorityDotClass = card.PriorityColorClass switch {
|
||||
"danger" => "board-priority-danger",
|
||||
"warning" => "board-priority-warning",
|
||||
"success" => "board-priority-success",
|
||||
"info" => "board-priority-info",
|
||||
_ => "board-priority-secondary"
|
||||
};
|
||||
<a href="@Url.Action("Details", new { id = card.Id })"
|
||||
class="board-card@(card.IsOverdue ? " board-card-hot" : "")"
|
||||
data-job-id="@card.Id"
|
||||
onclick="return false">
|
||||
|
||||
<div class="d-flex align-items-start justify-content-between gap-1">
|
||||
<span class="card-job-number">@card.JobNumber</span>
|
||||
</div>
|
||||
|
||||
<div class="card-customer">@card.CustomerName</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(card.Description))
|
||||
{
|
||||
<div class="card-desc">@(card.Description.Length > 60 ? card.Description[..60] + "…" : card.Description)</div>
|
||||
}
|
||||
|
||||
<div class="card-footer-row">
|
||||
<span class="board-priority-dot @priorityDotClass" title="@card.PriorityDisplayName"></span>
|
||||
|
||||
@if (card.IsOverdue)
|
||||
{
|
||||
<span class="mono" style="font-size:.7rem;color:var(--pcl-bad);font-weight:600">@card.DueDate!.Value.ToString("MMM d")</span>
|
||||
}
|
||||
else if (card.DueDate.HasValue)
|
||||
{
|
||||
<span class="mono" style="font-size:.7rem;color:var(--pcl-steel)">@card.DueDate.Value.ToString("MMM d")</span>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(card.AssignedWorkerName))
|
||||
{
|
||||
<span class="ms-auto mono" style="font-size:.68rem;color:var(--pcl-steel)" title="@card.AssignedWorkerName">
|
||||
@card.AssignedWorkerName.Split(' ')[0]
|
||||
</span>
|
||||
}
|
||||
else if (card.FinalPrice > 0)
|
||||
{
|
||||
<span class="ms-auto mono" style="font-size:.68rem;color:var(--pcl-steel)">@card.FinalPrice.ToString("C0")</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Toast for move feedback *@
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:1100">
|
||||
<div id="moveToast" class="toast align-items-center border-0" role="alert" aria-live="assertive">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="moveToastMsg"></div>
|
||||
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/lib/sortablejs/Sortable.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const COMPANY_ID = '@(User.FindFirst("CompanyId")?.Value ?? "0")';
|
||||
|
||||
// ── Show Completed persistence ───────────────────────────────────────────
|
||||
const TERMINAL_KEY = `jobBoard_showTerminal_${COMPANY_ID}`;
|
||||
const currentTerminal = @(showTerminal ? "true" : "false");
|
||||
const storedTerminal = localStorage.getItem(TERMINAL_KEY) === 'true';
|
||||
|
||||
// On first load, if stored preference differs from current URL state, redirect
|
||||
if (storedTerminal !== currentTerminal && !sessionStorage.getItem('boardTerminalRedirected')) {
|
||||
sessionStorage.setItem('boardTerminalRedirected', '1');
|
||||
window.location.href = '@Url.Action("Board")' + (storedTerminal ? '?showTerminal=true' : '');
|
||||
} else {
|
||||
sessionStorage.removeItem('boardTerminalRedirected');
|
||||
}
|
||||
|
||||
// Save preference when user clicks the toggle
|
||||
document.getElementById('toggleTerminalBtn')?.addEventListener('click', function () {
|
||||
localStorage.setItem(TERMINAL_KEY, String(!currentTerminal));
|
||||
});
|
||||
|
||||
// ── Column visibility ────────────────────────────────────────────────────
|
||||
const STORAGE_KEY = `jobBoard_hiddenCols_${COMPANY_ID}`;
|
||||
const hiddenColCountBadge = document.getElementById('hiddenColCount');
|
||||
|
||||
function loadHiddenCols() {
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
function saveHiddenCols(ids) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
|
||||
}
|
||||
|
||||
function applyVisibility() {
|
||||
const hidden = loadHiddenCols();
|
||||
let hiddenCount = 0;
|
||||
|
||||
document.querySelectorAll('.board-column').forEach(col => {
|
||||
const id = parseInt(col.dataset.statusId);
|
||||
const isHidden = hidden.includes(id);
|
||||
col.classList.toggle('col-hidden', isHidden);
|
||||
if (isHidden) hiddenCount++;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.col-vis-check').forEach(cb => {
|
||||
cb.checked = !hidden.includes(parseInt(cb.dataset.statusId));
|
||||
});
|
||||
|
||||
if (hiddenCount > 0) {
|
||||
hiddenColCountBadge.textContent = hiddenCount;
|
||||
hiddenColCountBadge.style.display = '';
|
||||
} else {
|
||||
hiddenColCountBadge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.col-vis-check').forEach(cb => {
|
||||
cb.addEventListener('change', () => {
|
||||
const id = parseInt(cb.dataset.statusId);
|
||||
const hidden = loadHiddenCols();
|
||||
if (cb.checked) {
|
||||
const idx = hidden.indexOf(id);
|
||||
if (idx > -1) hidden.splice(idx, 1);
|
||||
} else {
|
||||
if (!hidden.includes(id)) hidden.push(id);
|
||||
}
|
||||
saveHiddenCols(hidden);
|
||||
applyVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('showAllCols').addEventListener('click', () => {
|
||||
saveHiddenCols([]);
|
||||
applyVisibility();
|
||||
});
|
||||
|
||||
// Apply on load
|
||||
applyVisibility();
|
||||
|
||||
// ── Column reordering ────────────────────────────────────────────────────
|
||||
const COL_ORDER_KEY = `jobBoard_colOrder_${COMPANY_ID}`;
|
||||
|
||||
function loadColOrder() {
|
||||
try { return JSON.parse(localStorage.getItem(COL_ORDER_KEY) || '[]'); }
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
function saveColOrder() {
|
||||
const order = [...document.querySelectorAll('#boardScroll .board-column')]
|
||||
.map(c => parseInt(c.dataset.statusId));
|
||||
localStorage.setItem(COL_ORDER_KEY, JSON.stringify(order));
|
||||
}
|
||||
|
||||
function applyColOrder() {
|
||||
const order = loadColOrder();
|
||||
if (!order.length) return;
|
||||
const boardScroll = document.getElementById('boardScroll');
|
||||
order.forEach(id => {
|
||||
const col = boardScroll.querySelector(`.board-column[data-status-id="${id}"]`);
|
||||
if (col) boardScroll.appendChild(col); // moves to end in saved sequence
|
||||
});
|
||||
}
|
||||
|
||||
Sortable.create(document.getElementById('boardScroll'), {
|
||||
animation: 150,
|
||||
handle: '.col-drag-handle',
|
||||
draggable: '.board-column',
|
||||
ghostClass: 'col-sortable-ghost',
|
||||
onEnd() { saveColOrder(); }
|
||||
});
|
||||
|
||||
applyColOrder();
|
||||
|
||||
// ── Drag & drop + card navigation ────────────────────────────────────────
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
|
||||
?? document.cookie.split('; ').find(c => c.startsWith('XSRF-TOKEN'))?.split('=')[1] ?? '';
|
||||
|
||||
const toast = new bootstrap.Toast(document.getElementById('moveToast'), { delay: 2500 });
|
||||
const toastMsg = document.getElementById('moveToastMsg');
|
||||
|
||||
function showToast(msg, success) {
|
||||
const el = document.getElementById('moveToast');
|
||||
el.classList.remove('bg-success', 'bg-danger', 'text-white');
|
||||
el.classList.add(success ? 'bg-success' : 'bg-danger', 'text-white');
|
||||
toastMsg.textContent = msg;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
// Initialise SortableJS on every column body
|
||||
document.querySelectorAll('.board-column-body').forEach(colEl => {
|
||||
Sortable.create(colEl, {
|
||||
group: 'board', // shared group = drag between columns
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'dragging',
|
||||
|
||||
onEnd(evt) {
|
||||
const card = evt.item;
|
||||
const jobId = parseInt(card.dataset.jobId);
|
||||
const newColEl = evt.to;
|
||||
const newStatus = parseInt(newColEl.closest('.board-column').dataset.statusId);
|
||||
const oldColEl = evt.from;
|
||||
|
||||
// No-op if dropped in same column
|
||||
if (newColEl === oldColEl && evt.newIndex === evt.oldIndex) return;
|
||||
|
||||
// Update column counts immediately
|
||||
updateCount(oldColEl);
|
||||
updateCount(newColEl);
|
||||
|
||||
// AJAX move
|
||||
card.style.opacity = '.5';
|
||||
card.style.pointerEvents = 'none';
|
||||
|
||||
fetch('@Url.Action("MoveCard")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': token
|
||||
},
|
||||
body: JSON.stringify({ jobId, newStatusId: newStatus })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Update card's priority border stays — status shown by column
|
||||
showToast(`Moved to ${data.newStatusDisplay}`, true);
|
||||
} else {
|
||||
// Revert
|
||||
oldColEl.insertBefore(card, oldColEl.children[evt.oldIndex] ?? null);
|
||||
updateCount(oldColEl);
|
||||
updateCount(newColEl);
|
||||
showToast(data.message || 'Move failed', false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
oldColEl.insertBefore(card, oldColEl.children[evt.oldIndex] ?? null);
|
||||
updateCount(oldColEl);
|
||||
updateCount(newColEl);
|
||||
showToast('Network error — move reverted', false);
|
||||
})
|
||||
.finally(() => {
|
||||
card.style.opacity = '';
|
||||
card.style.pointerEvents = '';
|
||||
});
|
||||
},
|
||||
|
||||
onMove(evt) {
|
||||
evt.related.classList.remove('drag-over');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make cards navigate to Details on click (not drag)
|
||||
document.querySelectorAll('.board-card').forEach(card => {
|
||||
let moved = false;
|
||||
card.addEventListener('mousedown', () => { moved = false; });
|
||||
card.addEventListener('mousemove', () => { moved = true; });
|
||||
card.addEventListener('click', () => {
|
||||
if (!moved) window.location = card.href;
|
||||
});
|
||||
});
|
||||
|
||||
function updateCount(colEl) {
|
||||
const col = colEl.closest('.board-column');
|
||||
const badge = col.querySelector('.col-count');
|
||||
if (badge) badge.textContent = colEl.querySelectorAll('.board-card').length;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user