Rebuilt from Cursor transcript: tickets-workspace.js, tickets-detail-panel.js, app.js delegation, CSS, desk-live-stub. VM122 deploy pending SSH.
497 lines
20 KiB
JavaScript
497 lines
20 KiB
JavaScript
/**
|
|
* Tickets Workspace — P0 lista (8 KPIs + 3 sinais) · P1 filas · P2 KPI click + softRefresh
|
|
* Arquitectura separada; app.js delega render aqui.
|
|
*/
|
|
const TicketsWorkspace = {
|
|
context: null,
|
|
searchQuery: '',
|
|
queueFilter: null,
|
|
_pageReady: false,
|
|
_listFingerprint: '',
|
|
|
|
QUEUE_CHIPS: [
|
|
{ key: 'live', label: 'Live agora', icon: '🟢' },
|
|
{ key: 'stale', label: 'Parados', icon: '⏸' },
|
|
{ key: 'unassigned', label: 'Sem dono', icon: '👤' },
|
|
{ key: 'billing', label: 'Billing', icon: '💳' },
|
|
{ key: 'wazuh', label: 'Wazuh', icon: '⚠️' },
|
|
{ key: 'escalated', label: 'Escalados', icon: '🚨' },
|
|
],
|
|
|
|
async loadContext() {
|
|
const user = typeof getUser === 'function' ? getUser() : null;
|
|
const [presence, funnel, summary] = await Promise.all([
|
|
window.DeskLive?.enabled()
|
|
? api('/v1/live/presence').catch(() => ({ sessions: [] }))
|
|
: Promise.resolve({ sessions: [] }),
|
|
api('/v1/onboard/funnel?window_hours=48').catch(() => ({ active_sessions: [] })),
|
|
api('/v1/desk/summary').catch(() => ({})),
|
|
]);
|
|
const liveBySession = {};
|
|
for (const s of presence.sessions || []) {
|
|
if (s.session_id) liveBySession[s.session_id] = s;
|
|
}
|
|
const funnelBySession = {};
|
|
for (const s of funnel.active_sessions || []) {
|
|
if (s.session_id) funnelBySession[s.session_id] = s;
|
|
}
|
|
this.context = {
|
|
liveBySession,
|
|
funnelBySession,
|
|
staleHours: summary.onboard_stale_hours ?? 24,
|
|
user,
|
|
};
|
|
return this.context;
|
|
},
|
|
|
|
stripEnrichment(t) {
|
|
const out = { ...t };
|
|
for (const k of Object.keys(out)) {
|
|
if (k.startsWith('_')) delete out[k];
|
|
}
|
|
return out;
|
|
},
|
|
|
|
enrichTicket(t) {
|
|
const ctx = this.context || { liveBySession: {}, funnelBySession: {}, staleHours: 24 };
|
|
const sid = (t.session_id || '').trim();
|
|
const live = sid ? ctx.liveBySession[sid] : null;
|
|
const funnel = sid ? ctx.funnelBySession[sid] : null;
|
|
const isActive = ['open', 'escalated', 'assisting', 'resolved'].includes(t.status);
|
|
const lastAt = funnel?.last_event_at || t.created_at;
|
|
let idleHours = 0;
|
|
if (lastAt) {
|
|
idleHours = (Date.now() - new Date(lastAt).getTime()) / 3600000;
|
|
}
|
|
const stale = funnel?.stale || (isActive && idleHours >= ctx.staleHours);
|
|
const stage = funnel?.current_stage || t.lead_funnel_stage || t.event;
|
|
return {
|
|
...t,
|
|
_live: Boolean(live),
|
|
_livePath: (live?.path && !String(live.path).startsWith('/api/')) ? live.path : (funnel?.current_path && !String(funnel.current_path).startsWith('/api/') ? funnel.current_path : null),
|
|
_stage: stage,
|
|
_stageLabel: typeof FUNNEL_LABELS !== 'undefined' ? (FUNNEL_LABELS[stage] || stage) : stage,
|
|
_idleHours: Math.round(idleHours),
|
|
_stale: stale && isActive,
|
|
_unassigned: isActive && !t.assigned_to && !t.assisted_by,
|
|
_billing: Boolean(t.billing_state) || (t.subject || '').includes('[billing'),
|
|
_wazuh: t.source === 'wazuh' || t.event === 'wazuh.alert',
|
|
_carbonioHint: (t.subject || '').toLowerCase().includes('carbonio')
|
|
|| (t.subject || '').toLowerCase().includes('account_exists'),
|
|
};
|
|
},
|
|
|
|
computeIndicators(tickets) {
|
|
const enriched = tickets.map((t) => this.enrichTicket(t));
|
|
const active = enriched.filter((t) => ['open', 'escalated', 'assisting', 'resolved'].includes(t.status));
|
|
return {
|
|
open: active.filter((t) => t.status === 'open').length,
|
|
assisting: enriched.filter((t) => t.status === 'assisting').length,
|
|
escalated: enriched.filter((t) => t.status === 'escalated').length,
|
|
live: enriched.filter((t) => t._live).length,
|
|
unassigned: active.filter((t) => t._unassigned).length,
|
|
stale: active.filter((t) => t._stale).length,
|
|
billing: enriched.filter((t) => t._billing).length,
|
|
wazuh: enriched.filter((t) => t._wazuh).length,
|
|
total: enriched.length,
|
|
};
|
|
},
|
|
|
|
indicatorsHtml(ind) {
|
|
const items = [
|
|
{ key: 'open', label: 'Abertos', value: ind.open, tone: 'info' },
|
|
{ key: 'assisting', label: 'Assistindo', value: ind.assisting, tone: 'brand' },
|
|
{ key: 'escalated', label: 'Escalados', value: ind.escalated, tone: 'danger' },
|
|
{ key: 'live', label: 'Live agora', value: ind.live, tone: 'live' },
|
|
{ key: 'unassigned', label: 'Sem dono', value: ind.unassigned, tone: 'warn' },
|
|
{ key: 'stale', label: 'Parados', value: ind.stale, tone: 'muted' },
|
|
{ key: 'billing', label: 'Billing', value: ind.billing, tone: 'billing', icon: '💳' },
|
|
{ key: 'wazuh', label: 'Wazuh', value: ind.wazuh, tone: 'security', icon: '⚠️' },
|
|
];
|
|
return `
|
|
<div class="tickets-kpi-strip" role="group" aria-label="Indicadores da fila">
|
|
${items.map((it) => `
|
|
<button type="button"
|
|
class="tickets-kpi tickets-kpi--${it.tone}${this.queueFilter === it.key ? ' active' : ''}"
|
|
data-ticket-kpi="${it.key}"
|
|
title="Filtrar: ${esc(it.label)}">
|
|
<span class="tickets-kpi-value">${it.icon && it.value ? `${it.icon} ` : ''}${it.value}</span>
|
|
<span class="tickets-kpi-label">${esc(it.label)}</span>
|
|
</button>`).join('')}
|
|
</div>`;
|
|
},
|
|
|
|
queueChipsHtml() {
|
|
return `
|
|
<div class="tickets-queue-bar" role="group" aria-label="Filas inteligentes">
|
|
<span class="tickets-queue-label">Filas:</span>
|
|
${this.QUEUE_CHIPS.map((c) => `
|
|
<button type="button"
|
|
class="tickets-queue-chip${this.queueFilter === c.key ? ' active' : ''}"
|
|
data-ticket-queue="${c.key}">
|
|
${c.icon} ${esc(c.label)}
|
|
</button>`).join('')}
|
|
${this.queueFilter ? '<button type="button" class="tickets-queue-clear" data-ticket-queue-clear">Limpar fila</button>' : ''}
|
|
</div>`;
|
|
},
|
|
|
|
phaseSignal(t) {
|
|
if (t._stale) return { text: `parado ${t._idleHours}h`, cls: 'stale' };
|
|
if (t._stageLabel && t._stageLabel !== t.event) {
|
|
const short = String(t._stageLabel).replace(/validado|aplicado|criada/gi, '').trim() || t._stageLabel;
|
|
return { text: short.slice(0, 28), cls: 'phase' };
|
|
}
|
|
if (t.event) return { text: String(t.event).replace('onboarding.', '').replace('.', ' '), cls: 'phase' };
|
|
return { text: '—', cls: 'muted' };
|
|
},
|
|
|
|
ticketCardHtml(t) {
|
|
const phase = this.phaseSignal(t);
|
|
const isOnboard = t.source === 'vm112-onboard' || t.event?.startsWith?.('onboarding');
|
|
const title = t.event === 'wazuh.alert'
|
|
? esc(t.description || t.agent || t.subject)
|
|
: t.domain
|
|
? esc(t.domain)
|
|
: isOnboard
|
|
? `Onboarding · ${esc(t._stageLabel || 'VM112')}`
|
|
: esc(t.subject || `Ticket #${t.id}`);
|
|
const metaParts = [];
|
|
metaParts.push(`#${t.id}`);
|
|
if (t.wizard_ticket_id) metaParts.push(esc(t.wizard_ticket_id));
|
|
if (t.session_id) metaParts.push(sessionHashHtml(t.session_id, { full: false }));
|
|
if (t.email) metaParts.push(esc(t.email));
|
|
if (t.assigned_to || t.assisted_by) metaParts.push(esc(t.assisted_by || t.assigned_to));
|
|
metaParts.push(fmtDate(t.created_at));
|
|
const icons = [
|
|
t._billing ? '<span class="ticket-icon-chip" title="Billing">💳</span>' : '',
|
|
t._carbonioHint ? '<span class="ticket-icon-chip" title="Carbonio">🔒</span>' : '',
|
|
t._wazuh ? `<span class="ticket-icon-chip" title="Wazuh">${severityBadge(t.severity) || '⚠️'}</span>` : '',
|
|
t.crm_track === 'lead' ? '<span class="ticket-icon-chip ticket-icon-chip--lead">LEAD</span>' : '',
|
|
].filter(Boolean).join('');
|
|
const selected = state.selectedTicketId === t.id;
|
|
return `
|
|
<button type="button"
|
|
class="ticket-card ticket-card--${t.status}${selected ? ' selected' : ''}${t._live ? ' ticket-card--live' : ''}"
|
|
data-id="${t.id}"${t.session_id ? ` data-session="${esc(t.session_id)}"` : ''}
|
|
data-live="${t._live ? '1' : '0'}" data-stale="${t._stale ? '1' : '0'}">
|
|
<span class="ticket-card-rail ticket-card-rail--${t.status}" aria-hidden="true"></span>
|
|
<span class="ticket-card-main">
|
|
<span class="ticket-card-signals">
|
|
<span class="ticket-signal ticket-signal--status ticket-signal--${t.status}">${esc(statusLabel(t.status))}</span>
|
|
<span class="ticket-signal ticket-signal--live ${t._live ? 'is-live' : 'is-offline'}">
|
|
<i aria-hidden="true"></i>${t._live ? 'LIVE' : 'offline'}</span>
|
|
<span class="ticket-signal ticket-signal--phase ticket-signal--${phase.cls}">${esc(phase.text)}</span>
|
|
</span>
|
|
<span class="ticket-card-title">${title}</span>
|
|
<span class="ticket-card-meta">${metaParts.join(' · ')}</span>
|
|
${t._live && t._livePath ? `<span class="ticket-card-livepath" data-live-path>${esc(t._livePath)}</span>` : ''}
|
|
${icons ? `<span class="ticket-card-icons">${icons}</span>` : ''}
|
|
</span>
|
|
<span class="ticket-card-aside">${sourceBadge(t.source)}</span>
|
|
</button>`;
|
|
},
|
|
|
|
filterByQueue(tickets) {
|
|
if (!this.queueFilter) return tickets;
|
|
const rules = {
|
|
open: (t) => t.status === 'open',
|
|
assisting: (t) => t.status === 'assisting',
|
|
escalated: (t) => t.status === 'escalated',
|
|
live: (t) => t._live,
|
|
unassigned: (t) => t._unassigned,
|
|
stale: (t) => t._stale,
|
|
billing: (t) => t._billing,
|
|
wazuh: (t) => t._wazuh,
|
|
};
|
|
const fn = rules[this.queueFilter];
|
|
return fn ? tickets.filter(fn) : tickets;
|
|
},
|
|
|
|
filterBySearch(tickets) {
|
|
const raw = (this.searchQuery || '').trim();
|
|
if (!raw) return tickets;
|
|
const q = raw.toLowerCase();
|
|
const ticketIdQuery = q.replace(/^ticket\s*#?/, '').replace(/^#/, '').trim();
|
|
|
|
return tickets.filter((t) => {
|
|
if (/^\d+$/.test(ticketIdQuery) && String(t.id) === ticketIdQuery) return true;
|
|
const wiz = (t.wizard_ticket_id || '').toLowerCase();
|
|
if (wiz && (wiz === q || wiz.includes(q))) return true;
|
|
const liveIp = this.context?.liveBySession?.[t.session_id]?.client_ip;
|
|
const hay = [
|
|
t.id,
|
|
`#${t.id}`,
|
|
`ticket ${t.id}`,
|
|
t.subject,
|
|
t.domain,
|
|
t.email,
|
|
t.session_id,
|
|
t.agent,
|
|
t.description,
|
|
t.assigned_to,
|
|
t.assisted_by,
|
|
t.wizard_ticket_id,
|
|
liveIp,
|
|
].filter(Boolean).join(' ').toLowerCase();
|
|
return hay.includes(q);
|
|
});
|
|
},
|
|
|
|
applyFilters(tickets) {
|
|
return this.filterBySearch(this.filterByQueue(tickets));
|
|
},
|
|
|
|
listFingerprint(tickets) {
|
|
return tickets.map((t) => [
|
|
t.id, t.status, t._live ? 1 : 0, t._stale ? 1 : 0, t._stage, t._idleHours, t._livePath || '',
|
|
].join(':')).join('|');
|
|
},
|
|
|
|
setQueueFilter(key) {
|
|
this.queueFilter = this.queueFilter === key ? null : key;
|
|
const bar = document.getElementById('tickets-queue-bar');
|
|
if (bar) {
|
|
bar.innerHTML = this.queueChipsHtml();
|
|
this.bindQueueBar(bar);
|
|
}
|
|
this.syncQueueUi();
|
|
this.renderListOnly();
|
|
},
|
|
|
|
syncQueueUi() {
|
|
document.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
|
|
el.classList.toggle('active', el.dataset.ticketKpi === this.queueFilter);
|
|
});
|
|
document.querySelectorAll('[data-ticket-queue]').forEach((el) => {
|
|
el.classList.toggle('active', el.dataset.ticketQueue === this.queueFilter);
|
|
});
|
|
const clearBtn = document.querySelector('[data-ticket-queue-clear]');
|
|
if (clearBtn) clearBtn.hidden = !this.queueFilter;
|
|
},
|
|
|
|
bindKpiStrip(strip) {
|
|
strip.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
|
|
el.addEventListener('click', () => this.setQueueFilter(el.dataset.ticketKpi));
|
|
});
|
|
},
|
|
|
|
bindQueueBar(bar) {
|
|
bar.querySelectorAll('[data-ticket-queue]').forEach((el) => {
|
|
el.addEventListener('click', () => this.setQueueFilter(el.dataset.ticketQueue));
|
|
});
|
|
bar.querySelector('[data-ticket-queue-clear]')?.addEventListener('click', () => {
|
|
this.queueFilter = null;
|
|
this.syncQueueUi();
|
|
this.renderListOnly();
|
|
});
|
|
},
|
|
|
|
bindList(listEl) {
|
|
listEl.querySelectorAll('.ticket-card').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
state.selectedTicketId = Number(btn.dataset.id);
|
|
state.selectedSessionId = btn.dataset.session || null;
|
|
listEl.querySelectorAll('.ticket-card').forEach((r) => r.classList.remove('selected'));
|
|
btn.classList.add('selected');
|
|
if (window.TicketsDetailPanel) TicketsDetailPanel.render(state.selectedTicketId, document.getElementById('ticket-detail'));
|
|
else if (typeof renderTicketDetail === 'function') renderTicketDetail();
|
|
});
|
|
});
|
|
},
|
|
|
|
mountSearchToolbar() {
|
|
let bar = document.getElementById('tickets-search-bar');
|
|
if (!bar) {
|
|
const toolbar = document.querySelector('#view-tickets .toolbar');
|
|
if (!toolbar) return;
|
|
bar = document.createElement('div');
|
|
bar.id = 'tickets-search-bar';
|
|
bar.className = 'tickets-search-bar';
|
|
bar.innerHTML = `
|
|
<input type="search" id="tickets-search-input" class="tickets-search-input"
|
|
placeholder="Buscar ticket #, domínio, e-mail, sessão, OB-…, IP…" autocomplete="off" />
|
|
<span class="tickets-search-hint ticket-meta">Scan rápido · 3 sinais por card</span>`;
|
|
toolbar.parentNode.insertBefore(bar, toolbar);
|
|
bar.querySelector('#tickets-search-input')?.addEventListener('input', (e) => {
|
|
this.searchQuery = e.target.value;
|
|
if (state.view === 'tickets') this.renderListOnly();
|
|
});
|
|
}
|
|
},
|
|
|
|
mountQueueBar() {
|
|
let bar = document.getElementById('tickets-queue-bar');
|
|
if (!bar) {
|
|
const view = document.getElementById('view-tickets');
|
|
const toolbar = view?.querySelector('.toolbar');
|
|
if (!toolbar) return;
|
|
bar = document.createElement('div');
|
|
bar.id = 'tickets-queue-bar';
|
|
toolbar.parentNode.insertBefore(bar, toolbar.nextSibling);
|
|
}
|
|
bar.innerHTML = this.queueChipsHtml();
|
|
this.bindQueueBar(bar);
|
|
},
|
|
|
|
updateKpiStrip(ind) {
|
|
const strip = document.getElementById('tickets-kpi-strip');
|
|
if (!strip) return;
|
|
const map = {
|
|
open: ind.open, assisting: ind.assisting, escalated: ind.escalated, live: ind.live,
|
|
unassigned: ind.unassigned, stale: ind.stale, billing: ind.billing, wazuh: ind.wazuh,
|
|
};
|
|
strip.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
|
|
const key = el.dataset.ticketKpi;
|
|
const val = map[key];
|
|
if (val == null) return;
|
|
const valueEl = el.querySelector('.tickets-kpi-value');
|
|
if (valueEl) {
|
|
const icon = (key === 'billing' && val) ? '💳 ' : (key === 'wazuh' && val) ? '⚠️ ' : '';
|
|
valueEl.textContent = `${icon}${val}`;
|
|
}
|
|
});
|
|
},
|
|
|
|
patchLiveSignals(listEl, tickets) {
|
|
const byId = {};
|
|
for (const t of tickets) byId[t.id] = t;
|
|
listEl.querySelectorAll('.ticket-card').forEach((card) => {
|
|
const t = byId[Number(card.dataset.id)];
|
|
if (!t) return;
|
|
const wasLive = card.dataset.live === '1';
|
|
const isLive = t._live;
|
|
if (wasLive !== isLive) {
|
|
card.dataset.live = isLive ? '1' : '0';
|
|
card.classList.toggle('ticket-card--live', isLive);
|
|
const sig = card.querySelector('.ticket-signal--live');
|
|
if (sig) {
|
|
sig.classList.toggle('is-live', isLive);
|
|
sig.classList.toggle('is-offline', !isLive);
|
|
sig.lastChild.textContent = isLive ? 'LIVE' : 'offline';
|
|
}
|
|
}
|
|
card.dataset.stale = t._stale ? '1' : '0';
|
|
const phase = card.querySelector('.ticket-signal--phase');
|
|
if (phase) {
|
|
const p = this.phaseSignal(t);
|
|
phase.textContent = p.text;
|
|
phase.className = `ticket-signal ticket-signal--phase ticket-signal--${p.cls}`;
|
|
}
|
|
const pathEl = card.querySelector('[data-live-path]');
|
|
if (t._live && t._livePath) {
|
|
if (pathEl) pathEl.textContent = t._livePath;
|
|
else {
|
|
const main = card.querySelector('.ticket-card-main');
|
|
if (main) {
|
|
const span = document.createElement('span');
|
|
span.className = 'ticket-card-livepath';
|
|
span.dataset.livePath = '';
|
|
span.textContent = t._livePath;
|
|
main.appendChild(span);
|
|
}
|
|
}
|
|
} else if (pathEl) pathEl.remove();
|
|
});
|
|
},
|
|
|
|
renderListHtml(tickets) {
|
|
return tickets.length
|
|
? `<div class="ticket-card-list">${tickets.map((t) => this.ticketCardHtml(t)).join('')}</div>`
|
|
: '<p class="loading">Nenhum ticket neste filtro</p>';
|
|
},
|
|
|
|
async renderListOnly() {
|
|
const listEl = document.getElementById('ticket-list');
|
|
if (!listEl || !state.tickets?.length) return;
|
|
const enriched = state.tickets.map((t) => this.enrichTicket(this.stripEnrichment(t)));
|
|
state.tickets = enriched;
|
|
const filtered = this.applyFilters(enriched);
|
|
const fp = this.listFingerprint(filtered);
|
|
if (fp === this._listFingerprint && listEl.querySelector('.ticket-card')) {
|
|
this.patchLiveSignals(listEl, filtered);
|
|
this.updateKpiStrip(this.computeIndicators(enriched));
|
|
return;
|
|
}
|
|
this._listFingerprint = fp;
|
|
listEl.innerHTML = this.renderListHtml(filtered);
|
|
this.bindList(listEl, filtered);
|
|
this.updateKpiStrip(this.computeIndicators(enriched));
|
|
},
|
|
|
|
async softRefresh() {
|
|
if (!this._pageReady || state.view !== 'tickets') return;
|
|
try {
|
|
await this.loadContext();
|
|
const enriched = (state.tickets || []).map((t) => this.enrichTicket(this.stripEnrichment(t)));
|
|
state.tickets = enriched;
|
|
const ind = this.computeIndicators(enriched);
|
|
this.updateKpiStrip(ind);
|
|
const listEl = document.getElementById('ticket-list');
|
|
if (listEl) {
|
|
const filtered = this.applyFilters(enriched);
|
|
const fp = this.listFingerprint(filtered);
|
|
if (fp === this._listFingerprint && listEl.querySelector('.ticket-card')) {
|
|
this.patchLiveSignals(listEl, filtered);
|
|
} else {
|
|
this._listFingerprint = fp;
|
|
const selectedId = state.selectedTicketId;
|
|
listEl.innerHTML = this.renderListHtml(filtered);
|
|
this.bindList(listEl, filtered);
|
|
if (selectedId) {
|
|
listEl.querySelector(`.ticket-card[data-id="${selectedId}"]`)?.classList.add('selected');
|
|
}
|
|
}
|
|
}
|
|
if (state.selectedTicketId && TicketsDetailPanel?.activeTab === 'live') {
|
|
const detailEl = document.getElementById('ticket-detail');
|
|
const sid = detailEl?.dataset?.sessionId || state.selectedSessionId;
|
|
const trail = detailEl?.querySelector('#ticket-detail-live-trail');
|
|
if (sid && trail && window.DeskLive?.renderNavigationTab) {
|
|
await DeskLive.renderNavigationTab(sid, trail);
|
|
}
|
|
}
|
|
} catch { /* ignore poll errors */ }
|
|
},
|
|
|
|
async renderPage({ listEl, detailEl, tickets }) {
|
|
this.mountSearchToolbar();
|
|
this.mountQueueBar();
|
|
await this.loadContext();
|
|
const enriched = tickets.map((t) => this.enrichTicket(t));
|
|
state.tickets = enriched;
|
|
const ind = this.computeIndicators(enriched);
|
|
const filtered = this.applyFilters(enriched);
|
|
this._listFingerprint = this.listFingerprint(filtered);
|
|
|
|
let strip = document.getElementById('tickets-kpi-strip');
|
|
if (!strip) {
|
|
strip = document.createElement('div');
|
|
strip.id = 'tickets-kpi-strip';
|
|
const view = document.getElementById('view-tickets');
|
|
const searchBar = document.getElementById('tickets-search-bar');
|
|
if (view && searchBar) view.insertBefore(strip, searchBar);
|
|
else if (view) {
|
|
const toolbar = view.querySelector('.toolbar');
|
|
if (toolbar) view.insertBefore(strip, toolbar);
|
|
}
|
|
}
|
|
strip.innerHTML = this.indicatorsHtml(ind);
|
|
this.bindKpiStrip(strip);
|
|
|
|
listEl.innerHTML = this.renderListHtml(filtered);
|
|
this.bindList(listEl, filtered);
|
|
this._pageReady = true;
|
|
|
|
if (state.selectedTicketId && window.TicketsDetailPanel) {
|
|
await TicketsDetailPanel.render(state.selectedTicketId, detailEl);
|
|
} else if (state.selectedTicketId && typeof renderTicketDetail === 'function') {
|
|
await renderTicketDetail();
|
|
} else if (state.selectedSessionId && typeof renderSessionDetail === 'function') {
|
|
await renderSessionDetail();
|
|
} else if (detailEl) {
|
|
detailEl.innerHTML = '<div class="card detail-panel"><p class="empty">Selecione um ticket para ver detalhes</p></div>';
|
|
}
|
|
},
|
|
};
|
|
|
|
window.TicketsWorkspace = TicketsWorkspace;
|