/** * purchase-orders.js * Handles dynamic line-item management for Purchase Order Create/Edit forms. * A single searchable text input per row: type to filter inventory items or * enter a free-text description. Selecting an inventory item stores its ID in * a hidden field; typing something custom leaves the hidden field empty. */ 'use strict'; let inventoryData = []; let itemIndex = 0; document.addEventListener('DOMContentLoaded', () => { const dataEl = document.getElementById('inventoryItemsData'); if (dataEl) { try { inventoryData = JSON.parse(dataEl.textContent); } catch (e) { inventoryData = []; } } const tbody = document.getElementById('lineItemsBody'); if (tbody) { itemIndex = tbody.querySelectorAll('tr[data-index]').length; } updateTotals(); }); // ─── Add / Remove rows ──────────────────────────────────────────────────────── function addItem() { const tbody = document.getElementById('lineItemsBody'); if (!tbody) return; const i = itemIndex++; const tr = document.createElement('tr'); tr.setAttribute('data-index', i); tr.innerHTML = buildRowHtml(i, {}); tbody.appendChild(tr); toggleEmptyMessage(); updateTotals(); tr.querySelector('.item-search-input')?.focus(); } function removeItem(btn) { const tr = btn.closest('tr'); if (tr) { tr.remove(); reindexRows(); updateTotals(); toggleEmptyMessage(); } } function reindexRows() { const rows = document.querySelectorAll('#lineItemsBody tr[data-index]'); rows.forEach((tr, newIndex) => { tr.setAttribute('data-index', newIndex); tr.querySelectorAll('[name]').forEach(el => { el.name = el.name.replace(/Items\[\d+\]/, `Items[${newIndex}]`); }); const searchInput = tr.querySelector('.item-search-input'); if (searchInput) { searchInput.setAttribute('data-row-index', newIndex); searchInput.setAttribute('oninput', `onItemSearch(this, ${newIndex})`); } }); itemIndex = rows.length; } // ─── Row HTML builder ───────────────────────────────────────────────────────── /** * opts: { type: 'inventory'|'custom', selectedId, description, qty, cost, notes } */ function buildRowHtml(i, opts = {}) { const isInventory = opts.type === 'inventory' && opts.selectedId; const qty = opts.qty ?? 1; const cost = opts.cost ?? 0; const notes = opts.notes ?? ''; const lineTotal = (qty * cost).toFixed(2); let displayText = ''; if (isInventory) { const inv = inventoryData.find(x => String(x.value) === String(opts.selectedId)); displayText = inv ? inv.text : (opts.description || ''); } else { displayText = opts.description || ''; } return ` $${lineTotal} `; } // ─── Autocomplete ───────────────────────────────────────────────────────────── // Uses a single body-level dropdown so it's never clipped by table overflow. let _activeSearchInput = null; function getAutocompleteDropdown() { let el = document.getElementById('po-autocomplete'); if (!el) { el = document.createElement('ul'); el.id = 'po-autocomplete'; el.className = 'list-group shadow'; el.style.cssText = 'position:fixed;z-index:9999;max-height:220px;overflow-y:auto;' + 'margin:0;padding:0;border-radius:.35rem;min-width:180px;display:none;'; document.body.appendChild(el); } return el; } function onItemSearch(input, rowIndex) { _activeSearchInput = input; // Clear previously selected inventory item when user edits the field const tr = input.closest('tr'); const idField = tr?.querySelector('.inventory-id-field'); if (idField) idField.value = ''; const query = input.value.trim().toLowerCase(); const dropdown = getAutocompleteDropdown(); if (!query) { dropdown.style.display = 'none'; return; } const matches = inventoryData .filter(item => item.text.toLowerCase().includes(query)) .slice(0, 12); if (matches.length === 0) { dropdown.style.display = 'none'; return; } dropdown.innerHTML = matches.map(item => `
  • ${escHtml(item.text)}
  • `).join(''); // Position below the input const rect = input.getBoundingClientRect(); dropdown.style.top = (rect.bottom + 2) + 'px'; dropdown.style.left = rect.left + 'px'; dropdown.style.width = rect.width + 'px'; dropdown.style.display = ''; } function selectInventoryItem(li, rowIndex) { const tr = document.querySelector(`#lineItemsBody tr[data-index="${rowIndex}"]`); if (!tr) return; const searchInput = tr.querySelector('.item-search-input'); const idField = tr.querySelector('.inventory-id-field'); const costInput = tr.querySelector(`[name="Items[${rowIndex}].UnitCost"]`); if (searchInput) searchInput.value = li.dataset.text; if (idField) idField.value = li.dataset.value; if (costInput) { const cost = parseFloat(li.dataset.cost); if (cost > 0) costInput.value = cost.toFixed(2); } getAutocompleteDropdown().style.display = 'none'; _activeSearchInput = null; updateLineTotals(); } function hideAutocomplete() { // Delay so mousedown on a list item fires before blur hides the dropdown setTimeout(() => { getAutocompleteDropdown().style.display = 'none'; }, 150); } // Reposition on scroll/resize so the dropdown tracks the input window.addEventListener('scroll', () => { if (!_activeSearchInput) return; const rect = _activeSearchInput.getBoundingClientRect(); const dd = document.getElementById('po-autocomplete'); if (dd && dd.style.display !== 'none') { dd.style.top = (rect.bottom + 2) + 'px'; dd.style.left = rect.left + 'px'; } }, true); // ─── Totals ─────────────────────────────────────────────────────────────────── function updateLineTotals() { const rows = document.querySelectorAll('#lineItemsBody tr[data-index]'); let subTotal = 0; rows.forEach(tr => { const i = tr.getAttribute('data-index'); const qty = parseFloat(tr.querySelector(`[name="Items[${i}].QuantityOrdered"]`)?.value) || 0; const cost = parseFloat(tr.querySelector(`[name="Items[${i}].UnitCost"]`)?.value) || 0; const lineTotal = qty * cost; subTotal += lineTotal; const cell = tr.querySelector('.item-line-total'); if (cell) cell.textContent = '$' + lineTotal.toFixed(2); }); updateTotals(subTotal); } function updateTotals(subTotal) { if (subTotal === undefined) { subTotal = 0; document.querySelectorAll('#lineItemsBody tr[data-index]').forEach(tr => { const i = tr.getAttribute('data-index'); const qty = parseFloat(tr.querySelector(`[name="Items[${i}].QuantityOrdered"]`)?.value) || 0; const cost = parseFloat(tr.querySelector(`[name="Items[${i}].UnitCost"]`)?.value) || 0; subTotal += qty * cost; }); } const shipping = parseFloat(document.getElementById('shippingCostInput')?.value) || 0; const grandTotal = subTotal + shipping; setText('subTotalDisplay', '$' + subTotal.toFixed(2)); setText('grandTotalDisplay', '$' + grandTotal.toFixed(2)); } function setText(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; } function toggleEmptyMessage() { const tbody = document.getElementById('lineItemsBody'); const msg = document.getElementById('emptyItemsMessage'); const header = document.getElementById('lineItemsHeader'); if (!tbody || !msg) return; const hasRows = tbody.querySelectorAll('tr[data-index]').length > 0; msg.style.display = hasRows ? 'none' : 'block'; if (header) header.style.display = hasRows ? '' : 'none'; } // ─── Receive form helpers ───────────────────────────────────────────────────── function receiveAll() { document.querySelectorAll('.receive-qty-input').forEach(input => { input.value = parseFloat(input.dataset.remaining) || 0; }); } function clearAll() { document.querySelectorAll('.receive-qty-input').forEach(input => { input.value = 0; }); } // ─── Utilities ──────────────────────────────────────────────────────────────── function escHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }