482 lines
18 KiB
JavaScript
Executable file
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
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>`;
|
|
}
|