/* oven-scheduler.js — Batch scheduler with mouse-event drag (no HTML5 DnD API) */ 'use strict'; // ────────────────────────────────────────────────────────────────────────────── // Drag state // ────────────────────────────────────────────────────────────────────────────── let dragState = null; /* dragState shape: { type: 'queue' | 'batch', sourceEl: HTMLElement, // original row being dragged ghost: HTMLElement, // floating clone following the cursor offsetX, offsetY: number, // cursor offset within the source element // queue-specific: coatId, jobId, jobItemId, sqft, color, colorCode, pass, coatName, jobNumber, customer, description, priority, dueDate // batch-specific: batchItemId, sourceBatchId, sqft } */ // Currently highlighted batch card id (for visual feedback) let dragOverBatchId = null; // ────────────────────────────────────────────────────────────────────────────── // Init — wire up mousedown on all draggable rows // ────────────────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initDraggable(); }); function initDraggable() { // Queue coat rows document.querySelectorAll('[data-coat-id]').forEach(el => { el.addEventListener('mousedown', onQueueMouseDown); }); // Batch item rows (only those with draggable=true, i.e. editable batches) document.querySelectorAll('[data-batch-item-id]').forEach(el => { if (el.dataset.draggable === 'true' || el.getAttribute('draggable') === 'true') { el.addEventListener('mousedown', onBatchItemMouseDown); } }); } // ────────────────────────────────────────────────────────────────────────────── // Mousedown handlers // ────────────────────────────────────────────────────────────────────────────── function onQueueMouseDown(e) { if (e.button !== 0) return; // left button only if (e.target.closest('button')) return; // don't intercept button clicks const el = e.currentTarget; const rect = el.getBoundingClientRect(); dragState = { type: 'queue', sourceEl: el, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, coatId: +el.dataset.coatId, jobId: +el.dataset.jobId, jobItemId: +el.dataset.jobItemId, sqft: +el.dataset.sqft, color: el.dataset.color, colorCode: el.dataset.colorCode, pass: +el.dataset.pass, coatName: el.dataset.coatName, jobNumber: el.dataset.jobNumber, customer: el.dataset.customer, description: el.dataset.description, priority: el.dataset.priority, dueDate: el.dataset.dueDate }; startDrag(el, e); e.preventDefault(); } function onBatchItemMouseDown(e) { if (e.button !== 0) return; if (e.target.closest('button')) return; const el = e.currentTarget; const rect = el.getBoundingClientRect(); dragState = { type: 'batch', sourceEl: el, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, batchItemId: +el.dataset.batchItemId, sourceBatchId: +el.dataset.batchId, sqft: +el.dataset.sqft }; startDrag(el, e); e.preventDefault(); } // ────────────────────────────────────────────────────────────────────────────── // Ghost element // ────────────────────────────────────────────────────────────────────────────── function startDrag(sourceEl, e) { const rect = sourceEl.getBoundingClientRect(); const ghost = sourceEl.cloneNode(true); ghost.style.cssText = ` position: fixed; left: ${rect.left}px; top: ${rect.top}px; width: ${rect.width}px; pointer-events: none; z-index: 9999; opacity: 0.85; box-shadow: 0 8px 24px rgba(0,0,0,.25); border-radius: 6px; background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); padding: .3rem .5rem; font-size: .82rem; transition: none; `; document.body.appendChild(ghost); dragState.ghost = ghost; sourceEl.style.opacity = '0.35'; } function moveGhost(e) { if (!dragState?.ghost) return; dragState.ghost.style.left = (e.clientX - dragState.offsetX) + 'px'; dragState.ghost.style.top = (e.clientY - dragState.offsetY) + 'px'; } // ────────────────────────────────────────────────────────────────────────────── // Document mousemove — move ghost and highlight drop targets // ────────────────────────────────────────────────────────────────────────────── document.addEventListener('mousemove', e => { if (!dragState) return; moveGhost(e); // Hide ghost temporarily so elementFromPoint sees what's underneath dragState.ghost.style.display = 'none'; const elUnder = document.elementFromPoint(e.clientX, e.clientY); dragState.ghost.style.display = ''; const batchCard = elUnder?.closest('.batch-card'); const newId = batchCard ? +batchCard.dataset.batchId : null; if (newId !== dragOverBatchId) { // Remove old highlight if (dragOverBatchId !== null) { document.getElementById(`batch-${dragOverBatchId}`)?.classList.remove('drag-over'); document.getElementById(`empty-items-${dragOverBatchId}`)?.classList.remove('drag-over'); } // Add new highlight if (newId !== null) { batchCard.classList.add('drag-over'); document.getElementById(`empty-items-${newId}`)?.classList.add('drag-over'); } dragOverBatchId = newId; } // Highlight empty oven zone when a queue item hovers over it const ovenZone = elUnder?.closest('[data-oven-id]'); document.querySelectorAll('.drop-zone-empty').forEach(z => z.classList.remove('drag-over')); if (dragState.type === 'queue' && !batchCard && ovenZone) { document.getElementById(`empty-${ovenZone.dataset.ovenId}`)?.classList.add('drag-over'); } // Highlight queue when a batch item is being dragged (indicates it can be dropped there) const queueContainer = document.getElementById('queueContainer'); const overQueue = elUnder?.closest('#queueContainer') || elUnder?.closest('.scheduler-queue'); if (dragState.type === 'batch') { queueContainer?.classList.toggle('drag-over', !!overQueue); } else { queueContainer?.classList.remove('drag-over'); } }); // ────────────────────────────────────────────────────────────────────────────── // Document mouseup — commit the drop // ────────────────────────────────────────────────────────────────────────────── document.addEventListener('mouseup', e => { if (!dragState) return; if (e.button !== 0) return; // Clear all highlights if (dragOverBatchId !== null) { document.getElementById(`batch-${dragOverBatchId}`)?.classList.remove('drag-over'); document.getElementById(`empty-items-${dragOverBatchId}`)?.classList.remove('drag-over'); } document.getElementById('queueContainer')?.classList.remove('drag-over'); document.querySelectorAll('.drop-zone-empty').forEach(z => z.classList.remove('drag-over')); // Restore source element if (dragState.sourceEl) dragState.sourceEl.style.opacity = ''; // Remove ghost dragState.ghost?.remove(); // Find batch card under cursor dragState.ghost = null; const elUnder = document.elementFromPoint(e.clientX, e.clientY); const batchCard = elUnder?.closest('.batch-card'); const state = dragState; dragState = null; dragOverBatchId = null; if (!batchCard) { if (state.type === 'queue') { // Dropped on empty oven zone — auto-create batch and add const ovenZone = elUnder?.closest('[data-oven-id]'); if (ovenZone) autoCreateBatchAndAdd(+ovenZone.dataset.ovenId, state); } else if (state.type === 'batch') { // Dropped outside any batch card — return item to queue removeFromBatch(state.batchItemId, state.sourceBatchId); } return; } const batchId = +batchCard.dataset.batchId; if (state.type === 'queue') { addCoatToBatch(state, batchId); } else if (state.type === 'batch' && state.sourceBatchId !== batchId) { moveToBatch(state.batchItemId, batchId, state.sourceBatchId, state.sqft); } }); // ────────────────────────────────────────────────────────────────────────────── // Add coat from queue to batch // ────────────────────────────────────────────────────────────────────────────── async function addCoatToBatch(drag, batchId) { const res = await apiPost(URLS.addToBatch, { batchId, jobItemCoatId: drag.coatId }); if (!res.success) { showToast(res.error || 'Failed to add to batch', 'danger'); return; } // Remove from queue document.getElementById(`qcoat-${drag.coatId}`)?.remove(); // Add to batch items list appendBatchItemRow(batchId, res.item, true); // Update capacity display updateBatchCapacity(batchId, res.totalSurfaceAreaSqFt, res.capacityPct); // Update drop hint: switch from "Drag coats here" to "Drop more here" const hint = document.getElementById(`empty-items-${batchId}`); if (hint) { hint.classList.remove('batch-drop-hint-empty'); hint.innerHTML = 'Drop more here'; } updateQueueCount(-1); showToast('Added to batch', 'success'); } // ────────────────────────────────────────────────────────────────────────────── // Auto-create a batch then add the dragged coat (dropped on empty oven zone) // ────────────────────────────────────────────────────────────────────────────── async function autoCreateBatchAndAdd(ovenCostId, drag) { const createRes = await apiPost(URLS.createBatch, { ovenCostId, scheduledDate: SCHEDULED_DATE, scheduledStartTime: null }); if (!createRes.success) { showToast(createRes.error || 'Failed to create batch', 'danger'); return; } // Add the coat to the newly created batch before reloading const addRes = await apiPost(URLS.addToBatch, { batchId: createRes.batch.id, jobItemCoatId: drag.coatId }); if (!addRes.success) { showToast(addRes.error || 'Batch created but could not add coat', 'warning'); } else { showToast('Batch created and item added!', 'success'); } setTimeout(() => window.location.reload(), 600); } // ────────────────────────────────────────────────────────────────────────────── // Move batch item between batches // ────────────────────────────────────────────────────────────────────────────── async function moveToBatch(batchItemId, targetBatchId, sourceBatchId, sqft) { const res = await apiPost(URLS.moveToBatch, { batchItemId, targetBatchId }); if (!res.success) { showToast(res.error || 'Failed to move item', 'danger'); return; } // Move DOM element const itemEl = document.getElementById(`bitem-${batchItemId}`); const targetList = document.getElementById(`items-${targetBatchId}`); if (itemEl && targetList) { itemEl.dataset.batchId = targetBatchId; targetList.appendChild(itemEl); document.getElementById(`empty-items-${targetBatchId}`)?.remove(); } // Update capacity displays const sourceBatchCard = document.getElementById(`batch-${sourceBatchId}`); const maxSource = +(sourceBatchCard?.dataset.maxSqft || 0); updateBatchCapacity(sourceBatchId, res.sourceBatchTotal, maxSource > 0 ? Math.round(res.sourceBatchTotal / maxSource * 1000) / 10 : null); const targetBatchCard = document.getElementById(`batch-${targetBatchId}`); const maxTarget = +(targetBatchCard?.dataset.maxSqft || 0); updateBatchCapacity(targetBatchId, res.targetBatchTotal, maxTarget > 0 ? Math.round(res.targetBatchTotal / maxTarget * 1000) / 10 : null); showToast('Item moved', 'success'); } // ────────────────────────────────────────────────────────────────────────────── // Remove coat from batch back to queue (no page reload) // ────────────────────────────────────────────────────────────────────────────── async function removeFromBatch(batchItemId, batchId) { const res = await apiPost(URLS.removeFromBatch, { batchItemId }); if (!res.success) { showToast(res.error || 'Failed to remove', 'danger'); return; } document.getElementById(`bitem-${batchItemId}`)?.remove(); const batchCard = document.getElementById(`batch-${batchId}`); const maxSqft = +(batchCard?.dataset.maxSqft || 0); const capPct = maxSqft > 0 ? Math.round(res.batchTotal / maxSqft * 1000) / 10 : null; updateBatchCapacity(batchId, res.batchTotal, capPct); if (res.queueItem) returnCoatToQueue(res.queueItem); showToast('Removed — returned to queue', 'info'); } function returnCoatToQueue(q) { const coatElId = `qcoat-${q.jobItemCoatId}`; if (document.getElementById(coatElId)) return; const borderColors = { Rush: '#dc3545', Urgent: '#fd7e14', High: '#ffc107', Normal: '#0d6efd', Low: '#6c757d' }; const border = borderColors[q.priority] || '#6c757d'; const coatEl = document.createElement('div'); coatEl.className = 'batch-item-row d-flex align-items-center'; coatEl.id = coatElId; coatEl.dataset.coatId = q.jobItemCoatId; coatEl.dataset.jobId = q.jobId; coatEl.dataset.jobItemId = q.jobItemId; coatEl.dataset.sqft = q.surfaceAreaSqFt; coatEl.dataset.color = q.colorName || ''; coatEl.dataset.colorCode = q.colorCode || ''; coatEl.dataset.pass = q.coatPassNumber; coatEl.dataset.coatName = q.coatName; coatEl.dataset.jobNumber = q.jobNumber; coatEl.dataset.customer = q.customerName; coatEl.dataset.description = q.itemDescription; coatEl.dataset.priority = q.priority; coatEl.dataset.dueDate = q.dueDate || ''; const colorDot = q.colorName ? `` : ''; coatEl.innerHTML = `