asset_browser/public/modules/actions.js
setonc a558804026 Initial commit — asset browser web app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:06:25 -04:00

482 lines
18 KiB
JavaScript
Executable file

// 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 = `<div class="contact-loading">${spinner()} Loading contacts…</div>`;
try {
_contacts = await getContacts(_asset.customer_id);
_contacts.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
renderContactList(listEl, _contacts);
} catch (err) {
listEl.innerHTML = `<div class="contact-empty" style="color:var(--red)">Failed to load: ${esc(err.message)}</div>`;
}
}
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 = `<div class="contact-empty">No contacts found.</div>`;
return;
}
listEl.innerHTML = contacts.map(c => `
<div class="contact-item" data-contact-id="${c.id}" data-contact-name="${esc(c.name ?? '')}" data-no-refocus>
<div>
<div class="contact-item-name">${esc(c.name)}</div>
${c.email ? `<div class="contact-item-email">${esc(c.email)}</div>` : ''}
</div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--gray-400)" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div>
`).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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function spinner() {
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="animation:spin .7s linear infinite;display:inline-block;vertical-align:middle"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>`;
}