asset_browser/public/modules/ticketHistory.js
2026-03-27 09:18:39 -04:00

106 lines
4 KiB
JavaScript
Executable file

// ticketHistory.js — fetches recent tickets and renders them into the card.
// Filters to tickets belonging to the asset's assigned contact.
// Falls back to the 5 most recent customer tickets if no contact is assigned
// or if the contact has no tickets.
import { getTickets } from '../api/syncro.js';
let _loaded = false;
let _open = false;
export function initTicketHistory(asset) {
_loaded = false;
_open = false;
const toggle = document.getElementById('ticket-toggle');
const list = document.getElementById('ticket-list');
if (!toggle || !list) return;
toggle.classList.remove('open');
list.classList.remove('visible');
list.innerHTML = `<div class="contact-loading">${spinnerHTML()} Loading tickets…</div>`;
toggle.onclick = async () => {
_open = !_open;
toggle.classList.toggle('open', _open);
list.classList.toggle('visible', _open);
if (_open && !_loaded) {
await loadTickets(list, asset.customer_id, asset.contact_id ?? null);
_loaded = true;
}
};
// Wire up asset history toggle (static content, no async load needed)
const historyToggle = document.getElementById('history-toggle');
const historyList = document.getElementById('history-list');
if (historyToggle && historyList) {
let historyOpen = false;
historyToggle.classList.remove('open');
historyList.classList.remove('visible');
historyToggle.onclick = () => {
historyOpen = !historyOpen;
historyToggle.classList.toggle('open', historyOpen);
historyList.classList.toggle('visible', historyOpen);
};
}
}
async function loadTickets(listEl, customerId, contactId) {
listEl.innerHTML = `<div class="contact-loading">${spinnerHTML()} Loading tickets…</div>`;
try {
const all = await getTickets(customerId);
let tickets;
let scopeLabel;
if (contactId) {
const contactMatches = all.filter(t => t.contact_id === contactId);
if (contactMatches.length > 0) {
tickets = contactMatches.slice(0, 10);
scopeLabel = null; // no label needed — these are contact-scoped
} else {
// Contact exists but has no tickets — fall back to recent customer tickets
tickets = all.slice(0, 5);
scopeLabel = 'No tickets for assigned contact — showing recent customer tickets';
}
} else {
tickets = all.slice(0, 5);
scopeLabel = 'No contact assigned — showing recent customer tickets';
}
if (!tickets.length) {
listEl.innerHTML = `<div class="contact-empty">No tickets found.</div>`;
return;
}
listEl.innerHTML =
(scopeLabel ? `<div class="contact-empty" style="font-style:italic;margin-bottom:6px">${esc(scopeLabel)}</div>` : '') +
tickets.map(t => ticketItemHTML(t)).join('');
} catch (err) {
listEl.innerHTML = `<div class="contact-empty" style="color:var(--red)">Failed to load tickets: ${esc(err.message)}</div>`;
}
}
function ticketItemHTML(t) {
const statusKey = (t.status ?? '').toLowerCase().replace(/\s+/g, '-');
const date = t.created_at
? new Date(t.created_at).toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' })
: '';
const contactName = t.contact_fullname ? ` · ${esc(t.contact_fullname)}` : '';
return `
<div class="ticket-item status-${statusKey}">
<div>
<div class="ticket-subject">${esc(t.subject ?? t.problem_type ?? 'Ticket #' + (t.number ?? t.id))}</div>
<div class="ticket-meta">#${t.number ?? t.id} · ${date}${contactName}</div>
</div>
<div class="ticket-status ${statusKey || 'default'}">${esc(t.status ?? 'Unknown')}</div>
</div>`;
}
function spinnerHTML() {
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>`;
}
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}