0 selected

Order Detail

Loading...

Label Preview

Fulfillment Queue

Warehouse order processing

📦 Pack Station
Last sync: --
Loading summary...
📥
0
New Orders
📋
0
To Pick
📦
0
To Pack
🚚
0
Ready to Ship
⚠️
0
On Hold
Picking
Shipping
Export
Sync

Confirm Action

Are you sure?

Views:
Order # Client Items Status Source Age Actions
Loading orders...

Log Activity

Merchant
Activity Type
Amount (ZAR)
Description
' + '
Order ' + orderNum + ' — ' + items.length + ' items
' + '
' + '

📋 Pick List

Order ' + orderNum + '
' + '
Date: ' + dateStr + '
Time: ' + timeStr + '
Merchant: ' + merchant + '
Picker: ____________________
' + '
' + '
' + items.length + 'Line Items
' + '
' + totalQty + 'Total Qty
' + '
' + zoneNames.length + 'Zone' + (zoneNames.length !== 1 ? 's' : '') + '
' + '
' + '' + rows + '
#BinSKUProductQty
' + '
JLog Fulfillment — Order ' + orderNum + 'Printed ' + dateStr + ' ' + timeStr + '
' + '
'; var win = window.open('', '_blank'); if(win){ win.document.write(html); win.document.close(); } } window.confirmPack = async function(id){ const weight = parseFloat(document.getElementById('packWeight')?.value); const packageType = document.getElementById('packType')?.value; const length = parseFloat(document.getElementById('packLength')?.value) || null; const width = parseFloat(document.getElementById('packWidth')?.value) || null; const height = parseFloat(document.getElementById('packHeight')?.value) || null; if(!weight || weight <= 0){ showToast('Enter package weight', '#fbbf24'); return; } if(!packageType){ showToast('Select package type', '#fbbf24'); return; } try { const res = await fetch(`/api/fulfillment/orders/${id}/pack`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ weight, length, width, height, packageType }), }); if(!res.ok) throw new Error(await res.text()); showToast('Order packed — mark ready to ship when confirmed', '#2ee89e'); await Promise.all([loadOrders(), loadStats()]); await renderDetail(id); } catch(e){ showToast('Pack failed: ' + e.message, '#f87171'); } } window.confirmShip = async function(id){ // Called when label already exists — just mark shipped try { const orderRes = await fetch(`/api/fulfillment/orders/${id}`); const order = await orderRes.json(); const carrier = order.carrier || 'Unknown'; const trackingNumber = order.trackingNumber; if(!trackingNumber){ showToast('No tracking number on order', '#fbbf24'); return; } const res = await fetch(`/api/fulfillment/orders/${id}/ship`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ carrier, trackingNumber }), }); if(!res.ok) throw new Error(await res.text()); showToast('Order shipped!', '#2ee89e'); await Promise.all([loadOrders(), loadStats()]); await renderDetail(id); } catch(e){ showToast('Ship failed: ' + e.message, '#f87171'); } } window.confirmShipManual = async function(id){ const carrier = document.getElementById('manualCarrier')?.value; const trackingNumber = document.getElementById('manualTracking')?.value?.trim(); if(!carrier){ showToast('Select a carrier', '#fbbf24'); return; } if(!trackingNumber){ showToast('Enter tracking number', '#fbbf24'); return; } try { const res = await fetch(`/api/fulfillment/orders/${id}/ship`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ carrier, trackingNumber }), }); if(!res.ok) throw new Error(await res.text()); showToast('Order shipped!', '#2ee89e'); await Promise.all([loadOrders(), loadStats()]); await renderDetail(id); } catch(e){ showToast('Ship failed: ' + e.message, '#f87171'); } } window.generateLabel = async function(id){ const carrier = document.getElementById('shipCarrier')?.value; const serviceLevel = document.getElementById('shipService')?.value?.trim() || undefined; if(!carrier){ showToast('Select a carrier', '#fbbf24'); return; } const btn = document.getElementById('genLabelBtn'); if(btn){ btn.innerHTML = ' Generating...'; btn.disabled = true; } try { const res = await fetch(`/api/fulfillment/orders/${id}/generate-label`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ carrier, serviceLevel }), }); const data = await res.json(); if(!data.success){ showToast(data.error || 'Label generation failed', '#f87171'); if(btn){ btn.innerHTML = 'Generate Label'; btn.disabled = false; } return; } showToast(`Label generated: ${data.trackingNumber}`, '#2ee89e'); await Promise.all([loadOrders(), loadStats()]); await renderDetail(id); } catch(e){ showToast('Label failed: ' + e.message, '#f87171'); if(btn){ btn.innerHTML = 'Generate Label'; btn.disabled = false; } } } window.getRates = async function(id){ const btn = document.getElementById('getRatesBtn'); const container = document.getElementById('ratesContainer'); if(!container) return; if(btn){ btn.innerHTML = ' Getting rates...'; btn.disabled = true; } container.style.display = 'block'; container.innerHTML = '
Fetching rates from all carriers...
'; try { const res = await fetch(`/api/fulfillment/orders/${id}/rates`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({}), }); const data = await res.json(); if(data.error){ container.innerHTML = `
${data.error}
`; if(btn){ btn.innerHTML = 'Get Rate Quotes'; btn.disabled = false; } return; } const rates = data.rates || []; const suggestion = data.suggestion || {}; const suggestedCarrier = (suggestion.carrier || '').toLowerCase(); let rhtml = ''; if(suggestion.carrier){ rhtml += `
Recommended: ${suggestion.carrier} — ${suggestion.reason}
`; } if(rates.length === 0){ rhtml += '
No rates available for this destination.
'; } else { rhtml += '
'; rhtml += ''; rhtml += ''; let autoSelected = false; for(const r of rates){ const isRecommended = r.recommended; // Highlight rows matching the suggested carrier const matchesSuggestion = suggestedCarrier && ((r.carrierName || '').toLowerCase().includes(suggestedCarrier) || (r.serviceName || '').toLowerCase().includes(suggestedCarrier)); const rowBg = (isRecommended || matchesSuggestion) ? 'rgba(46,232,158,.08)' : 'transparent'; const priceStr = 'R' + (r.totalCostCents / 100).toFixed(2); const etaStr = r.etaDays ? r.etaDays + 'd' : '--'; const escapedService = (r.serviceName||'').replace(/'/g, "\\'"); const escapedSource = (r.source||'').replace(/'/g, "\\'"); const escapedSlug = (r.carrierSlug||'').replace(/'/g, "\\'"); const recBadge = matchesSuggestion ? ' RECOMMENDED' : (isRecommended ? ' ' : ''); rhtml += ``; rhtml += ``; rhtml += ``; rhtml += ``; rhtml += ``; rhtml += ''; // Auto-select the first matching suggested carrier if(matchesSuggestion && !autoSelected){ autoSelected = true; setTimeout(() => selectRate(r.source||'', r.serviceName||'', r.carrierName||'', r.carrierSlug||'', r.serviceCode||''), 100); } } rhtml += '
Carrier / ServicePriceETA
${r.serviceName || r.carrierName}${recBadge}${priceStr}${etaStr}
'; } container.innerHTML = rhtml; if(btn){ btn.innerHTML = 'Get Rate Quotes'; btn.disabled = false; } } catch(e){ container.innerHTML = `
Error: ${e.message}
`; if(btn){ btn.innerHTML = 'Get Rate Quotes'; btn.disabled = false; } } } window.selectRate = function(source, serviceName, carrierName, carrierSlug, serviceCode){ // Map source to carrier display name for the dropdown const carrierSelect = document.getElementById('shipCarrier'); const serviceInput = document.getElementById('shipService'); if(!carrierSelect) return; // Map carrier sources to dropdown values const sourceMap = { 'bobgo': 'Bob Go', 'mds': 'MDS Collivery', 'jetlog': 'JLog Internal', 'emit': 'EMIT', }; const carrierVal = sourceMap[source] || carrierName || ''; // Set carrier dropdown for(const opt of carrierSelect.options){ if(opt.value === carrierVal){ carrierSelect.value = carrierVal; break; } } // Set service code if(serviceInput) serviceInput.value = carrierSlug || serviceCode || ''; showToast(`Selected: ${serviceName}`, '#2ee89e'); // Collapse rates const container = document.getElementById('ratesContainer'); if(container) container.style.display = 'none'; } window.promptHold = function(id){ const reason = prompt('Reason for hold (optional):'); if(reason === null) return; // cancelled holdOrder(id, reason); } async function holdOrder(id, reason){ try { const res = await fetch(`/api/fulfillment/orders/${id}/hold`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ reason }), }); if(!res.ok) throw new Error(await res.text()); showToast('Order put on hold', '#f87171'); await Promise.all([loadOrders(), loadStats()]); await renderDetail(id); } catch(e){ showToast('Hold failed: ' + e.message, '#f87171'); } } window.markReadyToShip = async function(id){ await updateStatus(id, 'ready_to_ship'); await renderDetail(id); } window.openLabelModal = function(orderId, trackingNumber){ const backdrop = document.getElementById('labelModalBackdrop'); const frame = document.getElementById('labelModalFrame'); const downloadLink = document.getElementById('labelModalDownload'); const title = document.getElementById('labelModalTitle'); const url = '/api/fulfillment/orders/' + orderId + '/label'; frame.src = url; downloadLink.href = url; downloadLink.download = 'label-' + (trackingNumber || orderId) + '.pdf'; title.textContent = 'Label — ' + (trackingNumber || 'Preview'); backdrop.classList.add('open'); } window.closeLabelModal = function(){ document.getElementById('labelModalBackdrop').classList.remove('open'); document.getElementById('labelModalFrame').src = 'about:blank'; } window.printLabelModal = function(){ const frame = document.getElementById('labelModalFrame'); try { frame.contentWindow.print(); } catch(e){ window.open(frame.src, '_blank'); } } window.printPickLabel = function(orderId){ var order = allOrders.find(function(o){ return o.id === orderId; }); if(!order){ showToast('Order not found','error'); return; } var itemCount = (order.lineItems||[]).reduce(function(sum,li){ return sum + (li.quantity||1); }, 0); JLogNiimbotLabels.printPickLabel({ orderNumber: order.orderNumber || order.shipheroOrderId || '--', customerName: order.shopName || '', itemCount: itemCount }); } window.resumeOrder = async function(id){ await updateStatus(id, 'pending'); await renderDetail(id); } // ============ SEARCH ============ window.debounceSearch = function(){ clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { currentPage = 1; loadOrders(); }, 300); } // ============ CARD FILTER ============ window.clearCardFilter = function(){ if(activeCardFilter){ activeCardFilter = null; document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active')); } } window.filterByCard = function(filter){ console.log('[filterByCard] clicked:', filter, '| was:', activeCardFilter); const cards = document.querySelectorAll('.stat-card'); if(activeCardFilter === filter){ // Toggle off — deselect card and reset activeCardFilter = null; cards.forEach(c => c.classList.remove('active')); document.getElementById('statusFilter').value = ''; console.log('[filterByCard] toggled OFF, showing all'); } else { // Activate this card filter, clear dropdown activeCardFilter = filter; cards.forEach(c => c.classList.remove('active')); const card = document.querySelector(`[data-filter="${filter}"]`); if(card) card.classList.add('active'); else console.warn('[filterByCard] no stat-card found for data-filter:', filter); document.getElementById('statusFilter').value = ''; console.log('[filterByCard] activated:', filter); } currentPage = 1; loadOrders(); } // ============ BULK SELECT ============ window.toggleSelect = function(id){ if(selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); updateBulkBar(); } window.toggleSelectAll = function(){ const checked = document.getElementById('selectAll').checked; if(checked){ allOrders.forEach(o => selectedIds.add(o.id)); } else { allOrders.forEach(o => selectedIds.delete(o.id)); } renderTable(); updateBulkBar(); } function updateBulkBar(){ const bar = document.getElementById('bulkBar'); document.getElementById('bulkCount').textContent = selectedIds.size; if(selectedIds.size > 0) bar.classList.add('visible'); else bar.classList.remove('visible'); } window.clearBulk = function(){ selectedIds.clear(); document.getElementById('selectAll').checked = false; updateBulkBar(); renderTable(); } window.applyBulkAction = async function(action){ const ids = [...selectedIds]; if(ids.length === 0){ showToast('No orders selected', '#fbbf24'); return; } let reason; if(action === 'hold'){ reason = prompt('Reason for hold (optional):'); if(reason === null) return; } if(action === 'cancel'){ if(!await confirmAction('Cancel Orders', `Cancel ${ids.length} order(s)? This cannot be undone.`, { danger: true, okText: 'Cancel Orders' })) return; } try { const res = await fetch('/api/fulfillment/orders/bulk-action', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ orderIds: ids, action, reason }), }); const data = await res.json(); const msg = `${action}: ${data.success} succeeded, ${data.failed} failed`; showToast(msg, data.failed > 0 ? '#fbbf24' : '#2ee89e'); if(data.errors && data.errors.length > 0){ console.warn('Bulk action errors:', data.errors); } } catch(e){ showToast('Bulk action failed: ' + e.message, '#f87171'); } clearBulk(); await Promise.all([loadOrders(), loadStats()]); } // ============ SYNC ============ window.syncOrders = async function(){ const btn = document.getElementById('syncBtn'); btn.innerHTML = ' Syncing...'; btn.disabled = true; try { const res = await fetch('/api/sync/fulfillment/orders', {method:'POST'}); const data = await res.json(); showToast(`Synced: ${data.created||0} new, ${data.updated||0} updated`, '#2ee89e'); document.getElementById('lastSyncLabel').textContent = 'Last sync: just now'; await Promise.all([loadOrders(), loadStats()]); } catch(e){ showToast('Sync failed: ' + e.message, '#f87171'); } finally { btn.innerHTML = ' Sync ShipHero'; btn.disabled = false; } } // ============ TOAST ============ // ============ AUDIO FEEDBACK (BARCODE SCAN) ============ const _audioCtx = (function(){ try { return new (window.AudioContext || window.webkitAudioContext)(); } catch(e){ return null; } })(); function playBeep(freq, duration){ if(!_audioCtx) return; if(_audioCtx.state === 'suspended') _audioCtx.resume(); const osc = _audioCtx.createOscillator(); const gain = _audioCtx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.value = 0.15; osc.connect(gain); gain.connect(_audioCtx.destination); osc.start(); osc.stop(_audioCtx.currentTime + duration / 1000); } function beepSuccess(){ playBeep(800, 150); } function beepError(){ playBeep(400, 150); setTimeout(() => playBeep(400, 150), 300); } function beepAlreadyPicked(){ playBeep(600, 100); } // ============ BARCODE SCANNER LISTENER ============ (function(){ let scanBuffer = ''; let lastKeyTime = 0; const SCAN_THRESHOLD_MS = 50; const MIN_SCAN_LENGTH = 4; document.addEventListener('keydown', function(e){ // Skip if user is typing in an input/textarea (manual entry field handles its own Enter) const tag = (document.activeElement || {}).tagName; if(tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; const now = Date.now(); if(e.key === 'Enter'){ if(scanBuffer.length >= MIN_SCAN_LENGTH){ e.preventDefault(); handleBarcodeScan(scanBuffer); } scanBuffer = ''; lastKeyTime = 0; return; } // Only buffer printable single characters if(e.key.length !== 1) return; if(now - lastKeyTime > SCAN_THRESHOLD_MS && scanBuffer.length > 0){ // Gap too long — reset buffer (manual typing) scanBuffer = ''; } scanBuffer += e.key; lastKeyTime = now; }); // Manual SKU input handler (delegated since the input is rendered dynamically) document.addEventListener('keydown', function(e){ if(e.target && e.target.id === 'manualSkuInput' && e.key === 'Enter'){ e.preventDefault(); e.stopPropagation(); const val = e.target.value.trim(); if(val){ handleBarcodeScan(val); e.target.value = ''; } } }); })(); function showToast(msg, color){ const el = document.createElement('div'); el.className = 'toast'; el.style.background = color || 'var(--jl-surface)'; el.style.color = color === '#2ee89e' || color === '#fbbf24' ? '#0f1923' : '#fff'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 3000); } // ============ SHIPHERO HEALTH CHECK ============ async function checkShipheroHealth(){ try { const res = await fetch('/api/sync/fulfillment/status'); const d = await res.json(); const banner = document.getElementById('shipheroHealthBanner'); const msg = document.getElementById('shipheroHealthMsg'); const syncBtn = document.getElementById('syncBtn'); if(d.orders?.lastSyncAt){ const ago = timeAgo(d.orders.lastSyncAt); document.getElementById('lastSyncLabel').textContent = `Last sync: ${ago} ago`; } if(d.shipheroAvailable === false){ banner.style.display = 'flex'; msg.innerHTML = ' ShipHero is unreachable — showing cached data. New orders may not appear until connection is restored.'; if(window.lucide) lucide.createIcons(); if(syncBtn){ syncBtn.disabled = true; syncBtn.style.opacity = '.4'; syncBtn.title = 'ShipHero unavailable'; } } else { // Check staleness: last successful sync > 30 min ago const lastSync = d.orders?.lastSyncAt ? new Date(d.orders.lastSyncAt) : null; const staleMs = lastSync ? Date.now() - lastSync.getTime() : Infinity; if(staleMs > 30 * 60 * 1000){ const minsAgo = Math.round(staleMs / 60000); banner.style.display = 'flex'; msg.innerHTML = ` Data may be stale — last synced ${minsAgo} minutes ago`; if(window.lucide) lucide.createIcons(); } else { banner.style.display = 'none'; } if(syncBtn){ syncBtn.disabled = false; syncBtn.style.opacity = '1'; syncBtn.title = ''; } } } catch(e){} } // ============ OFFLINE HANDLING ============ const OFFLINE_QUEUE_KEY = 'jlog_fulfillment_offline_queue'; const FORM_DRAFT_KEY = 'jlog_fulfillment_draft'; function isOffline(){ return !navigator.onLine; } function getOfflineQueue(){ try { return JSON.parse(localStorage.getItem(OFFLINE_QUEUE_KEY) || '[]'); } catch(e){ return []; } } function saveOfflineQueue(queue){ localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue)); const countEl = document.getElementById('offlineQueueCount'); if(countEl){ countEl.style.display = queue.length > 0 ? 'inline' : 'none'; countEl.textContent = queue.length > 0 ? queue.length + ' queued' : ''; } } function queueOfflineAction(action, orderId, data){ const queue = getOfflineQueue(); queue.push({ action, orderId, data, timestamp: Date.now() }); saveOfflineQueue(queue); showToast('Action queued — will sync when online', '#fbbf24'); } async function processOfflineQueue(){ const queue = getOfflineQueue(); if(!queue.length) return; const banner = document.getElementById('onlineBanner'); banner.style.display = 'flex'; for(let i = 0; i < queue.length; i++){ banner.textContent = `Syncing ${i+1}/${queue.length}...`; const item = queue[i]; try { if(item.action === 'status'){ await fetch(`/api/fulfillment/orders/${item.orderId}/status`, { method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify(item.data), }); } else if(item.action === 'bulk'){ await fetch('/api/fulfillment/orders/bulk-action', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(item.data), }); } } catch(e){ showToast('Failed to sync queued action: ' + (item.action), '#f87171'); } } saveOfflineQueue([]); banner.innerHTML = ' All queued actions synced!'; if(window.lucide) lucide.createIcons(); setTimeout(() => { banner.style.display = 'none'; }, 3000); await Promise.all([loadOrders(), loadStats()]); } window.addEventListener('offline', () => { document.getElementById('offlineBanner').style.display = 'flex'; document.getElementById('onlineBanner').style.display = 'none'; }); window.addEventListener('online', () => { document.getElementById('offlineBanner').style.display = 'none'; processOfflineQueue(); }); // Auto-save form drafts every 10s setInterval(() => { const draft = {}; const fields = ['packWeight','packLength','packWidth','packHeight','packType','shipCarrier','shipService','manualCarrier','manualTracking']; for(const f of fields){ const el = document.getElementById(f); if(el) draft[f] = el.value; } if(Object.values(draft).some(v => v)) localStorage.setItem(FORM_DRAFT_KEY, JSON.stringify(draft)); }, 10000); // Restore draft on page load try { const draft = JSON.parse(localStorage.getItem(FORM_DRAFT_KEY) || '{}'); if(Object.values(draft).some(v => v)){ // Will restore when detail panel opens window._pendingDraft = draft; } } catch(e){} // Hook into detail render to restore drafts const origRenderDetail = renderDetail; renderDetail = async function(id){ await origRenderDetail(id); if(window._pendingDraft){ const draft = window._pendingDraft; let hasData = false; for(const [k,v] of Object.entries(draft)){ const el = document.getElementById(k); if(el && v){ el.value = v; hasData = true; } } if(hasData){ const restore = await confirmAction('Restore Draft', 'Restore unsaved work from previous session?', { okText: 'Restore' }); if(!restore){ for(const [k] of Object.entries(draft)){ const el = document.getElementById(k); if(el) el.value = ''; } localStorage.removeItem(FORM_DRAFT_KEY); } } window._pendingDraft = null; } }; // ============ TODAY'S WORK SUMMARY ============ const TW_HIDE_KEY = 'jlog-tw-hidden'; async function loadTodaySummary() { const wrap = document.getElementById('twWrap'); const showBtn = document.getElementById('twShowBtn'); const loading = document.getElementById('twLoading'); const content = document.getElementById('twContent'); // Check if hidden if (localStorage.getItem(TW_HIDE_KEY) === '1') { wrap.style.display = 'none'; showBtn.style.display = 'inline-flex'; return; } try { const res = await fetch('/api/fulfillment/today', { credentials: 'include' }); const data = await res.json(); // Get user name let userName = 'team'; try { const uRes = await fetch('/api/auth/me', { credentials: 'include' }); if (uRes.ok) { const u = await uRes.json(); userName = (u.name || u.email || 'team').split(/[\s@]/)[0]; } } catch(e) {} // Greeting + date document.getElementById('twGreeting').innerHTML = data.greeting + ', ' + userName.replace(//g,'>') + ' 👋'; const d = new Date(data.date + 'T12:00:00'); const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; document.getElementById('twDate').textContent = days[d.getDay()] + ', ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear(); // Task cards const s = data.summary || {}; const cards = [ { icon: '\uD83D\uDCCB', num: s.toPick || 0, label: 'To Pick', filter: 'to_pick', color: 'var(--jl-blue-text)' }, { icon: '\uD83D\uDCE6', num: s.toPack || 0, label: 'To Pack', filter: 'to_pack', color: 'rgb(180 83 9)' }, { icon: '\uD83D\uDE9A', num: s.toShip || 0, label: 'To Ship', filter: 'ready_to_ship', color: 'var(--jl-accent)' }, { icon: '\u26A0\uFE0F', num: s.onHold || 0, label: 'On Hold', filter: 'on_hold', color: 'var(--jl-red-text)' }, { icon: '\u2705', num: s.shippedToday || 0, label: 'Shipped', filter: 'shipped_today', color: 'rgba(46,232,158,.6)' }, ]; document.getElementById('twCards').innerHTML = cards.map(c => `
${c.icon} ${c.num} ${c.label}
` ).join(''); // Alerts const alertIcons = { urgent: '\uD83D\uDD34', warning: '\uD83D\uDFE1', info: '\uD83D\uDD35' }; const alertClasses = { urgent: 'tw-alert-urgent', warning: 'tw-alert-warning', info: 'tw-alert-info' }; const alerts = data.alerts || []; let alertsHtml = ''; if (alerts.length > 0) { alertsHtml = alerts.map(a => `
${alertIcons[a.type] || '\u2139\uFE0F'} ${a.message}
` ).join(''); } // Billing alerts (ADMIN/STAFF only) try { const uMe = await fetch('/api/auth/me', { credentials: 'include' }); if (uMe.ok) { const meData = await uMe.json(); // TEMP: billing alerts visible to all users during testing if (true || meData.role === 'ADMIN') { const bRes = await fetch('/api/billing/uninvoiced-summary', { credentials: 'include' }); if (bRes.ok) { const billing = await bRes.json(); if (billing.merchants && billing.merchants.length > 0) { alertsHtml += `
\uD83D\uDCB0 ${billing.merchants.length} merchant${billing.merchants.length !== 1 ? 's' : ''} with uninvoiced charges (R${billing.totalUninvoiced.toLocaleString('en-ZA', {minimumFractionDigits:2})})
`; const overdue = billing.merchants.filter(m => m.isOverdue); overdue.forEach(m => { alertsHtml += `
\uD83D\uDFE1 ${m.merchantName} \u2014 R${m.totalAmount.toLocaleString('en-ZA', {minimumFractionDigits:2})} uninvoiced (${m.daysSinceOldest} days, ${m.billingFrequency} cycle due)
`; }); } } } } } catch(e) { /* billing alerts non-critical */ } document.getElementById('twAlerts').innerHTML = alertsHtml; // Top priority const prio = data.topPriority || []; if (prio.length > 0) { let prioHtml = '
Top Priority
'; prio.forEach((p, i) => { const action = (p.status === 'picked') ? 'Pack' : 'Pick'; const actionLink = (p.status === 'picked') ? '/pack-station.html?order=' + p.id : '#'; const actionClick = (p.status === 'picked') ? `window.location.href='${actionLink}'` : `openDetail('${p.id}')`; prioHtml += `
${esc(p.orderNumber)} ${p.merchant} \u2014 ${p.reason} ${p.age} old
`; }); document.getElementById('twPrio').innerHTML = prioHtml; } else { document.getElementById('twPrio').innerHTML = ''; } loading.style.display = 'none'; content.style.display = 'block'; } catch(e) { console.error('Today summary error', e); loading.textContent = 'Could not load summary'; } } window.hideTodaySummary = function() { document.getElementById('twWrap').style.display = 'none'; document.getElementById('twShowBtn').style.display = 'inline-flex'; localStorage.setItem(TW_HIDE_KEY, '1'); }; window.showTodaySummary = function() { document.getElementById('twWrap').style.display = 'block'; document.getElementById('twShowBtn').style.display = 'none'; localStorage.removeItem(TW_HIDE_KEY); loadTodaySummary(); }; // ============ SAVED VIEWS ============ let savedViews = []; let activeViewId = null; let svContextViewId = null; async function loadSavedViews() { try { const res = await fetch('/api/views?page=fulfillment'); if (!res.ok) return; savedViews = await res.json(); renderSavedViews(); // On first load: check localStorage, then default view if (!activeViewId) { const storedId = localStorage.getItem('jlog_active_view_fulfillment'); const storedView = storedId && savedViews.find(v => v.id === storedId); if (storedView) { applySavedView(storedView.id); } else { const defaultView = savedViews.find(v => v.isDefault); if (defaultView) applySavedView(defaultView.id); } } } catch (e) { console.warn('Failed to load saved views', e); } } function renderSavedViews() { const container = document.getElementById('savedViewsList'); if (!container) return; container.innerHTML = savedViews.map(v => { const isActive = v.id === activeViewId; const iconName = v.icon || 'pin'; const iconHtml = ''; const shared = v.isShared ? '' : ''; return ` `; if (data.carriers.length > 1) { h += '
'; for (const c of data.carriers.slice(1, 3)) { h += ``; } h += '
'; } el.innerHTML = h; } catch (e) { /* silent */ } } // ============ FULFILLMENT ACTIVITY MODAL ============ var faOrderId = ''; var faMerchant = ''; window.openFulfillActivityModal = function(orderId, merchantName, orderNum) { faOrderId = orderId; faMerchant = merchantName; document.getElementById('faModalMerchant').value = merchantName; document.getElementById('faModalType').value = ''; document.getElementById('faModalAmount').value = ''; document.getElementById('faModalDesc').value = ''; document.getElementById('faModalOrderId').value = orderId; document.getElementById('faModalOrderNum').value = orderNum || ''; document.getElementById('faCalcPreview').style.display = 'none'; document.getElementById('fulfillActivityBackdrop').classList.add('open'); }; window.closeFulfillActivityModal = function() { document.getElementById('fulfillActivityBackdrop').classList.remove('open'); }; window.faTypeChange = async function() { var type = document.getElementById('faModalType').value; document.getElementById('faCalcPreview').style.display = 'none'; document.getElementById('faModalAmount').value = ''; // For simple types, no dynamic fields — just let them type amount }; window.faSubmit = async function() { var activityType = document.getElementById('faModalType').value; var amount = parseFloat(document.getElementById('faModalAmount').value); var description = document.getElementById('faModalDesc').value.trim(); if (!activityType) { showToast('Select an activity type', '#f87171'); return; } if (isNaN(amount) || amount < 0) { showToast('Enter a valid amount', '#f87171'); return; } if (!description) { showToast('Enter a description', '#f87171'); return; } var btn = document.getElementById('faModalSubmit'); btn.disabled = true; btn.innerHTML = ' Logging...'; try { var body = { merchantName: faMerchant, activityType: activityType, amount: amount, description: description, orderId: document.getElementById('faModalOrderId').value || undefined, orderNumber: document.getElementById('faModalOrderNum').value || undefined, }; var r = await fetch('/api/billing/activities', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), }); if (!r.ok) { var err = await r.json().catch(function(){ return {}; }); throw new Error(err.message || 'Failed to log activity'); } closeFulfillActivityModal(); showToast('Activity logged', '#2ee89e'); // Refresh detail to update charges count if (currentDetailId) await renderDetail(currentDetailId); } catch(e) { showToast('Error: ' + e.message, '#f87171'); } finally { btn.disabled = false; btn.textContent = 'Log Activity'; } }; // Initialize Lucide icons if(window.lucide) lucide.createIcons(); })();