// actions.js — wires up all action buttons on the asset card import { updateAsset, getContacts } from '../api/syncro.js'; import { showToast } from './toast.js'; import { resetIdleTimer, focusScanInput } from './scanner.js'; import { renderAssetCard } from './assetCard.js'; import { updateCachedAsset, getCustomerContacts } from './assetBrowser.js'; import { initTicketHistory } from './ticketHistory.js'; import { addToQueueAndOpen } from './labelCenter.js'; import { addToQueue } from './labelCenter.js'; let _asset = null; let _contacts = null; let _pendingAction = null; // 'change-owner' | 'sign-out' export function initActions(asset) { _asset = asset; _contacts = null; _pendingAction = null; // Possession toggle document.getElementById('action-toggle-possession')?.addEventListener('click', handleTogglePossession); // Lifecycle dropdown toggle document.getElementById('action-lifecycle')?.addEventListener('click', handleLifecycleClick); // Change owner document.getElementById('action-change-owner')?.addEventListener('click', () => openContactPanel('change-owner')); // Remove user document.getElementById('action-remove-user')?.addEventListener('click', handleRemoveUser); // Sign out document.getElementById('action-sign-out')?.addEventListener('click', () => openContactPanel('sign-out')); // Print label document.getElementById('action-print-label')?.addEventListener('click', () => addToQueueAndOpen(_asset)); // Add to Label Center queue document.getElementById('action-add-to-queue')?.addEventListener('click', () => addToQueue(_asset)); // Lifecycle dropdown options (action section) document.querySelectorAll('.lifecycle-option').forEach(btn => { btn.addEventListener('click', () => handleSetLifecycle(btn.dataset.stage)); }); // Status badge dropdowns (status section) document.getElementById('status-possession-btn')?.addEventListener('click', _handleStatusPossessionClick); document.getElementById('status-lifecycle-btn')?.addEventListener('click', _handleStatusLifecycleClick); document.querySelectorAll('.status-dropdown-option[data-possession]').forEach(btn => { btn.addEventListener('click', () => _handleSetPossession(btn.dataset.possession)); }); document.querySelectorAll('#status-lifecycle-dropdown .status-dropdown-option[data-stage]').forEach(btn => { btn.addEventListener('click', () => _handleSetLifecycleFromStatus(btn.dataset.stage)); }); // Close dropdowns when clicking outside document.addEventListener('click', _handleOutsideClick, { capture: true }); // Contact panel document.getElementById('contact-cancel')?.addEventListener('click', closeContactPanel); document.getElementById('contact-search')?.addEventListener('input', filterContacts); // Infrastructure document.getElementById('action-infrastructure')?.addEventListener('click', _openInfraPanel); document.getElementById('infra-cancel-btn')?.addEventListener('click', _closeInfraPanel); document.getElementById('infra-confirm-btn')?.addEventListener('click', _handleConfirmInfra); document.getElementById('infra-tag-input')?.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('infra-location-input')?.focus(); }); document.getElementById('infra-location-input')?.addEventListener('keydown', e => { if (e.key === 'Enter') _handleConfirmInfra(); }); } // ── Toggle Possession ───────────────────────────────────────────────────────── async function handleTogglePossession() { const btn = document.getElementById('action-toggle-possession'); const current = _asset?.properties?.['Possession Status']; const next = current === 'In IT Possession' ? 'Deployed' : 'In IT Possession'; const actionDesc = `Possession toggled to ${next}`; setButtonLoading(btn, true); try { const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); await updateAsset(_asset.id, { properties: { 'Possession Status': next, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.properties = { ...(_asset.properties ?? {}), 'Possession Status': next, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory }; refreshCard(); showToast(`Possession set to: ${next}`, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } // ── Lifecycle Stage ─────────────────────────────────────────────────────────── function handleLifecycleClick(e) { e.stopPropagation(); const btn = document.getElementById('action-lifecycle'); const dropdown = document.getElementById('lifecycle-dropdown'); const isOpen = dropdown?.classList.contains('open'); _closeLifecycleDropdown(); if (!isOpen) { dropdown?.classList.add('open'); btn?.classList.add('open'); } } function _closeLifecycleDropdown() { document.getElementById('lifecycle-dropdown')?.classList.remove('open'); document.getElementById('action-lifecycle')?.classList.remove('open'); } function _handleOutsideClick(e) { const wrap = document.querySelector('.lc-wrap'); if (wrap && !wrap.contains(e.target)) { _closeLifecycleDropdown(); } if (!e.target.closest('.status-badge-wrap')) { _closeAllStatusDropdowns(); } } // ── Status Badge Dropdowns ──────────────────────────────────────────────────── function _handleStatusPossessionClick(e) { e.stopPropagation(); const btn = document.getElementById('status-possession-btn'); const dropdown = document.getElementById('status-possession-dropdown'); const isOpen = dropdown?.classList.contains('open'); _closeAllStatusDropdowns(); if (!isOpen) { dropdown?.classList.add('open'); btn?.classList.add('open'); } } function _handleStatusLifecycleClick(e) { e.stopPropagation(); const btn = document.getElementById('status-lifecycle-btn'); const dropdown = document.getElementById('status-lifecycle-dropdown'); const isOpen = dropdown?.classList.contains('open'); _closeAllStatusDropdowns(); if (!isOpen) { dropdown?.classList.add('open'); btn?.classList.add('open'); } } function _closePossessionStatusDropdown() { document.getElementById('status-possession-dropdown')?.classList.remove('open'); document.getElementById('status-possession-btn')?.classList.remove('open'); } function _closeLifecycleStatusDropdown() { document.getElementById('status-lifecycle-dropdown')?.classList.remove('open'); document.getElementById('status-lifecycle-btn')?.classList.remove('open'); } function _closeAllStatusDropdowns() { _closePossessionStatusDropdown(); _closeLifecycleStatusDropdown(); } async function _handleSetPossession(next) { _closePossessionStatusDropdown(); const actionDesc = `Possession toggled to ${next}`; try { const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); await updateAsset(_asset.id, { properties: { 'Possession Status': next, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.properties = { ...(_asset.properties ?? {}), 'Possession Status': next, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory }; refreshCard(); showToast(`Possession set to: ${next}`, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } async function _handleSetLifecycleFromStatus(stage) { _closeLifecycleStatusDropdown(); const actionDesc = `Lifecycle moved to ${stage}`; try { const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); await updateAsset(_asset.id, { properties: { 'Lifecycle Stage': stage, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.properties = { ...(_asset.properties ?? {}), 'Lifecycle Stage': stage, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory }; refreshCard(); showToast(`Lifecycle stage: ${stage}`, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } async function handleSetLifecycle(stage) { _closeLifecycleDropdown(); const actionDesc = `Lifecycle moved to ${stage}`; try { const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); await updateAsset(_asset.id, { properties: { 'Lifecycle Stage': stage, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.properties = { ...(_asset.properties ?? {}), 'Lifecycle Stage': stage, 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory }; refreshCard(); showToast(`Lifecycle stage: ${stage}`, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } // ── Remove User ─────────────────────────────────────────────────────────────── async function handleRemoveUser() { const actionDesc = `User removed`; const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); try { await updateAsset(_asset.id, { contact_id: null, properties: { 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.contact_id = null; _asset.contact = null; _asset.contact_fullname = null; _asset.properties = { ...(_asset.properties ?? {}), 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory }; refreshCard(); showToast('User removed', 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } // ── Contact Panel ───────────────────────────────────────────────────────────── async function openContactPanel(action) { _pendingAction = action; const panel = document.getElementById('contact-panel'); const titleEl = document.getElementById('contact-panel-title'); const listEl = document.getElementById('contact-list'); const searchEl = document.getElementById('contact-search'); titleEl.textContent = action === 'sign-out' ? 'Sign Out — Select Employee' : 'Change User — Select Contact'; searchEl.value = ''; panel.classList.add('visible'); panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); // Close lifecycle dropdown if open _closeLifecycleDropdown(); if (_contacts) { renderContactList(listEl, _contacts); return; } // Use the browser-side contact cache if warm (avoids spinner on repeat opens) const cached = getCustomerContacts(_asset.customer_id); if (cached) { _contacts = cached; renderContactList(listEl, _contacts); return; } listEl.innerHTML = `
${spinner()} Loading contacts…
`; try { _contacts = await getContacts(_asset.customer_id); _contacts.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); renderContactList(listEl, _contacts); } catch (err) { listEl.innerHTML = `
Failed to load: ${esc(err.message)}
`; } } function closeContactPanel() { document.getElementById('contact-panel')?.classList.remove('visible'); _pendingAction = null; focusScanInput(); } // ── Infrastructure ──────────────────────────────────────────────────────────── function _openInfraPanel() { const panel = document.getElementById('infra-panel'); const titleEl = document.getElementById('infra-panel-title'); const isInfra = _asset.properties?.['Infrastructure'] === 'Yes'; if (titleEl) titleEl.textContent = isInfra ? 'Manage Infrastructure' : 'Set Infrastructure'; const tagInput = document.getElementById('infra-tag-input'); if (tagInput) tagInput.value = _asset.properties?.['Tag'] ?? ''; const locInput = document.getElementById('infra-location-input'); if (locInput) locInput.value = _asset.properties?.['Location'] ?? ''; panel?.classList.add('visible'); panel?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); _closeLifecycleDropdown(); tagInput?.focus(); } function _closeInfraPanel() { document.getElementById('infra-panel')?.classList.remove('visible'); focusScanInput(); } async function _handleConfirmInfra() { const tag = document.getElementById('infra-tag-input')?.value.trim(); const location = document.getElementById('infra-location-input')?.value.trim(); if (!tag) { document.getElementById('infra-tag-input')?.focus(); return; } _closeInfraPanel(); const actionDesc = `Marked as Infrastructure: ${tag}${location ? ` — ${location}` : ''}`; const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); try { await updateAsset(_asset.id, { contact_id: null, properties: { 'Infrastructure': 'Yes', 'Tag': tag, 'Location': location ?? '', 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }, _asset.customer_id); _asset.contact_id = null; _asset.contact = null; _asset.contact_fullname = null; _asset.properties = { ...(_asset.properties ?? {}), 'Infrastructure': 'Yes', 'Tag': tag, 'Location': location ?? '', 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }; refreshCard(); showToast(`Infrastructure: ${tag}${location ? ` — ${location}` : ''}`, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } function filterContacts() { if (!_contacts) return; const q = document.getElementById('contact-search')?.value.toLowerCase() ?? ''; const filtered = q ? _contacts.filter(c => c.name?.toLowerCase().includes(q) || c.email?.toLowerCase().includes(q)) : _contacts; renderContactList(document.getElementById('contact-list'), filtered); } function renderContactList(listEl, contacts) { if (!contacts.length) { listEl.innerHTML = `
No contacts found.
`; return; } listEl.innerHTML = contacts.map(c => `
${esc(c.name)}
${c.email ? `` : ''}
`).join(''); listEl.querySelectorAll('.contact-item').forEach(item => { item.addEventListener('click', () => handleContactSelected( parseInt(item.dataset.contactId), item.dataset.contactName )); }); } async function handleContactSelected(contactId, contactName) { closeContactPanel(); const isSignOut = _pendingAction === 'sign-out'; const actionDesc = isSignOut ? `Signed out to ${contactName}` : `Owner changed to ${contactName}`; const newHistory = appendHistory(_asset.properties?.['Asset History'], actionDesc); const updatePayload = { contact_id: contactId, properties: { 'Infrastructure': '', // clear if previously infrastructure 'Tag': '', 'Location': '', 'Last Scan Date': today(), 'Last Action': actionDesc, 'Asset History': newHistory, }, }; if (isSignOut) { updatePayload.properties['Possession Status'] = 'Deployed'; } try { await updateAsset(_asset.id, updatePayload, _asset.customer_id); // Update local asset state _asset.contact_id = contactId; _asset.contact = { id: contactId, name: contactName }; _asset.contact_fullname = contactName; _asset.properties = { ...(_asset.properties ?? {}), ...updatePayload.properties, }; refreshCard(); showToast(actionDesc, 'success'); } catch (err) { showToast('Failed: ' + err.message, 'error'); } finally { resetIdleTimer(); } } // ── Refresh card in place ───────────────────────────────────────────────────── function refreshCard() { renderAssetCard(_asset); initActions(_asset); initTicketHistory(_asset); updateCachedAsset(_asset); } // ── Helpers ─────────────────────────────────────────────────────────────────── function setButtonLoading(btn, loading) { if (!btn) return; btn.classList.toggle('loading', loading); } function today() { return new Date().toISOString().split('T')[0]; } function appendHistory(current, actionDesc) { const now = new Date(); const stamp = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0'); const newEntry = `[${stamp}] — ${actionDesc}`; const existing = (current ?? '').trim(); const lines = existing ? existing.split('\n').filter(Boolean) : []; lines.unshift(newEntry); return lines.slice(0, 100).join('\n'); } function esc(s) { return String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function spinner() { return ``; }