Rebuilt from Cursor transcript: tickets-workspace.js, tickets-detail-panel.js, app.js delegation, CSS, desk-live-stub. VM122 deploy pending SSH.
467 lines
21 KiB
JavaScript
467 lines
21 KiB
JavaScript
/**
|
|
* Tickets Detail Panel — abas Resumo | Ao vivo | Funil + próxima acção contextual (P1)
|
|
*/
|
|
const TicketsDetailPanel = {
|
|
activeTab: 'resumo',
|
|
lastTicketId: null,
|
|
mirrorTimer: null,
|
|
mirrorSessionId: null,
|
|
|
|
stopMirrorPoll() {
|
|
if (this.mirrorTimer) {
|
|
clearInterval(this.mirrorTimer);
|
|
this.mirrorTimer = null;
|
|
}
|
|
this.mirrorSessionId = null;
|
|
},
|
|
|
|
parseUiState(presence, uiState) {
|
|
if (uiState && typeof uiState === 'object' && Object.keys(uiState).length) return uiState;
|
|
if (presence?.ui_state) {
|
|
try { return JSON.parse(presence.ui_state); } catch { return {}; }
|
|
}
|
|
return {};
|
|
},
|
|
|
|
mirrorHtml(observer) {
|
|
if (!observer) {
|
|
return '<p class="loading">Carregando espelho do wizard…</p>';
|
|
}
|
|
if (observer.is_assisting) {
|
|
return '<p class="ticket-mirror-hint">ASM ativo — o cliente está pausado. Use <strong>Reabrir wizard ASM</strong> para continuar no modo técnico.</p>';
|
|
}
|
|
const ui = this.parseUiState(observer.presence, observer.ui_state);
|
|
const step = observer.step_label || ui.step_label || observer.presence?.wizard_step || observer.funnel_stage || '—';
|
|
const domain = observer.domain || ui.domain || '—';
|
|
const errRaw = observer.client_error || ui.error;
|
|
const err = errRaw && (observer.is_assisting || !String(errRaw).includes('controle interno do suporte'))
|
|
? errRaw
|
|
: null;
|
|
const modals = observer.modals || ui.modals || {};
|
|
const modalLines = [];
|
|
if (modals.support) modalLines.push('Modal <strong>Ajuda do Suporte</strong> aberto');
|
|
if (modals.dns) modalLines.push('Modal <strong>DNS</strong> aberto');
|
|
if (modals.dns_advanced) modalLines.push('Modal <strong>DNS avançado</strong> aberto');
|
|
const activity = (observer.activity_log || []).slice(-12).map((e) => {
|
|
const lvl = (e.level || 'info').toLowerCase();
|
|
return `<li class="ticket-mirror-log ticket-mirror-log--${esc(lvl)}"><time>${esc((e.ts || e.created_at || '').slice(11, 19) || '—')}</time> ${esc(e.message || e.msg || '')}</li>`;
|
|
}).join('');
|
|
const infraPending = (observer.infra_status?.steps || []).filter((s) => !s.ok).slice(0, 4)
|
|
.map((s) => esc(s.label || s.id)).join(' · ');
|
|
return `
|
|
<div class="ticket-mirror">
|
|
<p class="ticket-mirror-lead">O técnico lê aqui o mesmo contexto do cliente <strong>antes</strong> de assumir a sessão.</p>
|
|
<div class="ticket-mirror-screen">
|
|
<div class="ticket-mirror-screen__bar">
|
|
<span class="ticket-signal ticket-signal--live is-live"><i aria-hidden="true"></i>Cliente no wizard</span>
|
|
${observer.presence?.wizard_step || step !== '—' ? `<span class="ticket-mirror-step">${esc(step)}</span>` : ''}
|
|
</div>
|
|
<dl class="kv kv--compact ticket-mirror-kv">
|
|
<dt>Passo</dt><dd><strong>${esc(step)}</strong></dd>
|
|
<dt>Domínio</dt><dd>${esc(domain)}</dd>
|
|
${observer.wizard_ticket_id ? `<dt>Chamado wizard</dt><dd><code class="session-hash">${esc(observer.wizard_ticket_id)}</code></dd>` : ''}
|
|
${observer.client_note ? `<dt>Nota do cliente</dt><dd>${esc(observer.client_note)}</dd>` : ''}
|
|
</dl>
|
|
${err ? `<div class="ticket-mirror-error" role="alert"><strong>Erro na tela do cliente</strong><p>${esc(err)}</p></div>` : ''}
|
|
${modalLines.length ? `<div class="ticket-mirror-modals">${modalLines.join(' · ')}</div>` : ''}
|
|
${infraPending ? `<p class="ticket-mirror-infra"><span>Infra pendente:</span> ${infraPending}</p>` : ''}
|
|
<div class="ticket-mirror-terminal">
|
|
<div class="ticket-mirror-terminal__head">Terminal / activity log (cliente)</div>
|
|
<ol class="ticket-mirror-log-list">${activity || '<li class="ticket-meta">Sem linhas recentes</li>'}</ol>
|
|
</div>
|
|
</div>
|
|
<p class="ticket-meta ticket-mirror-updated">Atualizado ${fmtDate(observer.presence?.last_seen_at || new Date().toISOString())}</p>
|
|
</div>`;
|
|
},
|
|
|
|
async fetchObserverView(sessionId) {
|
|
return api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}/observer-view`);
|
|
},
|
|
|
|
async refreshMirrorPane(detailEl, sessionId) {
|
|
const pane = detailEl.querySelector('[data-ticket-pane="espelho"]');
|
|
if (!pane || pane.hidden) return;
|
|
try {
|
|
const observer = await this.fetchObserverView(sessionId);
|
|
pane.innerHTML = this.mirrorHtml(observer);
|
|
} catch (e) {
|
|
pane.innerHTML = `<p class="loading">Espelho indisponível: ${esc(e.message)}</p>`;
|
|
}
|
|
},
|
|
|
|
startMirrorPoll(detailEl, sessionId) {
|
|
this.stopMirrorPoll();
|
|
if (!sessionId) return;
|
|
this.mirrorSessionId = sessionId;
|
|
const tick = () => this.refreshMirrorPane(detailEl, sessionId);
|
|
tick();
|
|
this.mirrorTimer = setInterval(tick, 4000);
|
|
},
|
|
|
|
espelhoPaneHtml() {
|
|
return `
|
|
<div class="ticket-detail-pane" data-ticket-pane="espelho"${this.activeTab !== 'espelho' ? ' hidden' : ''}>
|
|
<p class="loading">Carregando espelho do wizard…</p>
|
|
</div>`;
|
|
},
|
|
|
|
computeNextAction(t, assistMeta, carbonioBlock) {
|
|
const enriched = window.TicketsWorkspace?.enrichTicket
|
|
? TicketsWorkspace.enrichTicket(t)
|
|
: t;
|
|
if (carbonioBlock?.status === 'pending') {
|
|
return {
|
|
tone: 'warn',
|
|
icon: '🔒',
|
|
title: 'Bloqueio Carbonio',
|
|
text: 'Remover conta órfã para o cliente repetir o passo',
|
|
cta: 'scroll-carbonio',
|
|
};
|
|
}
|
|
const assistStatus = normalizeAssistStatus(assistMeta?.assist_status || assistMeta?.ticket_status || t.status);
|
|
if (assistStatus === 'assisting' && typeof canAssist === 'function' && canAssist()) {
|
|
return {
|
|
tone: 'brand',
|
|
icon: '🖥️',
|
|
title: 'ASM ativo',
|
|
text: 'Fechou o wizard por engano? Reabra a sessão sem devolver ao cliente',
|
|
cta: 'resume-wizard',
|
|
};
|
|
}
|
|
if (enriched._live && typeof canAssist === 'function' && canAssist()
|
|
&& assistMeta?.can_escalate && assistStatus !== 'assisting') {
|
|
return {
|
|
tone: 'live',
|
|
icon: '🟢',
|
|
title: 'Cliente online',
|
|
text: enriched._livePath
|
|
? `Em ${enriched._livePath} — abra Espelho cliente e depois assuma`
|
|
: 'Abra a aba Espelho cliente para ver a mesma tela antes de assumir',
|
|
cta: 'takeover',
|
|
};
|
|
}
|
|
if (enriched._stale) {
|
|
return {
|
|
tone: 'stale',
|
|
icon: '⏸',
|
|
title: 'Sessão parada',
|
|
text: `Sem progresso há ~${enriched._idleHours}h — escalar ou contactar`,
|
|
cta: 'escalate',
|
|
};
|
|
}
|
|
if (enriched._unassigned && enriched.status === 'open') {
|
|
return {
|
|
tone: 'info',
|
|
icon: '👤',
|
|
title: 'Sem dono',
|
|
text: 'Ticket aberto sem técnico atribuído',
|
|
cta: 'takeover',
|
|
};
|
|
}
|
|
if (enriched._billing) {
|
|
return {
|
|
tone: 'billing',
|
|
icon: '💳',
|
|
title: 'Billing pendente',
|
|
text: 'Validar pagamento antes de prosseguir onboarding',
|
|
cta: null,
|
|
};
|
|
}
|
|
if (enriched._wazuh && (enriched.severity == null || enriched.severity >= 10)) {
|
|
return {
|
|
tone: 'danger',
|
|
icon: '⚠️',
|
|
title: 'Alerta Wazuh',
|
|
text: 'Investigar evento de segurança',
|
|
cta: null,
|
|
};
|
|
}
|
|
if (enriched.status === 'escalated') {
|
|
return {
|
|
tone: 'danger',
|
|
icon: '🚨',
|
|
title: 'Escalado',
|
|
text: 'Prioridade operacional — assumir ou resolver',
|
|
cta: 'takeover',
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
|
|
nextActionHtml(action) {
|
|
if (!action) return '';
|
|
const cta = action.cta === 'takeover'
|
|
? '<button type="button" class="btn btn-primary btn-sm" data-next-action="takeover">Assumir</button>'
|
|
: action.cta === 'resume-wizard'
|
|
? '<button type="button" class="btn btn-primary btn-sm" data-next-action="resume-wizard">Reabrir wizard</button>'
|
|
: action.cta === 'escalate'
|
|
? '<button type="button" class="btn btn-ghost btn-sm" data-next-action="escalate">Escalar</button>'
|
|
: action.cta === 'scroll-carbonio'
|
|
? '<button type="button" class="btn btn-primary btn-sm" data-next-action="scroll-carbonio">Resolver</button>'
|
|
: '';
|
|
return `
|
|
<div class="ticket-next-action ticket-next-action--${action.tone}">
|
|
<span class="ticket-next-action-icon" aria-hidden="true">${action.icon}</span>
|
|
<div class="ticket-next-action-body">
|
|
<strong>${esc(action.title)}</strong>
|
|
<span>${esc(action.text)}</span>
|
|
</div>
|
|
${cta}
|
|
</div>`;
|
|
},
|
|
|
|
tabsHtml({ t, sessionId, hasLive, hasFunil, hasEspelho }) {
|
|
const tabs = [
|
|
{ id: 'resumo', label: 'Resumo' },
|
|
];
|
|
if (hasEspelho) tabs.push({ id: 'espelho', label: 'Espelho cliente' });
|
|
if (hasLive) tabs.push({ id: 'live', label: 'Ao vivo' });
|
|
if (hasFunil) tabs.push({ id: 'funil', label: 'Funil' });
|
|
if (!tabs.some((x) => x.id === this.activeTab)) this.activeTab = 'resumo';
|
|
return `
|
|
<nav class="ticket-detail-tabs" role="tablist">
|
|
${tabs.map((tab) => `
|
|
<button type="button" role="tab"
|
|
class="ticket-detail-tab${this.activeTab === tab.id ? ' active' : ''}"
|
|
data-ticket-tab="${tab.id}"
|
|
aria-selected="${this.activeTab === tab.id}">
|
|
${esc(tab.label)}
|
|
</button>`).join('')}
|
|
</nav>`;
|
|
},
|
|
|
|
resumoHtml(t, { sessionId, assistMeta, carbonioBlock, timeline, timing }) {
|
|
const closeStatuses = ['open', 'escalated', 'assisting', 'resolved'];
|
|
return `
|
|
<div class="ticket-detail-pane" data-ticket-pane="resumo"${this.activeTab !== 'resumo' ? ' hidden' : ''}>
|
|
<dl class="kv">
|
|
<dt>Origem</dt><dd>${sourceBadge(t.source)}</dd>
|
|
<dt>Domínio/Agente</dt><dd>${esc(t.domain || t.agent || '—')}</dd>
|
|
<dt>Email</dt><dd>${esc(t.email || '—')}</dd>
|
|
${typeof ticketFunnelKvHtml === 'function' ? ticketFunnelKvHtml(t) : `<dt>Evento</dt><dd>${esc(t.event || '—')}</dd>`}
|
|
${t.assigned_to ? `<dt>Atribuído</dt><dd>${esc(t.assigned_to)}</dd>` : ''}
|
|
${t.assisted_by ? `<dt>Assistido por</dt><dd>${esc(t.assisted_by)}</dd>` : ''}
|
|
${t.client_paused ? '<dt>Cliente</dt><dd><span class="badge assisting">pausado</span></dd>' : ''}
|
|
${t.ready_for_ops ? '<dt>Ops</dt><dd><span class="badge ok">ready for ops</span></dd>' : ''}
|
|
${t.severity != null ? `<dt>Severidade</dt><dd>${severityBadge(t.severity)}</dd>` : ''}
|
|
${t.rule_id ? `<dt>Regra</dt><dd>${esc(t.rule_id)}</dd>` : ''}
|
|
${t.description ? `<dt>Descrição</dt><dd>${esc(t.description)}</dd>` : ''}
|
|
${t.desk_message ? `<dt>Nota</dt><dd>${esc(t.desk_message)}</dd>` : ''}
|
|
${t.registration_role ? `<dt>Perfil</dt><dd>${esc(roleLabel(t.registration_role))}</dd>` : ''}
|
|
${t.activation_url ? `<dt>Ativar conta</dt><dd><a class="btn btn-primary btn-sm" href="${esc(t.activation_url)}" target="_blank" rel="noopener">Abrir link de ativação</a></dd>` : ''}
|
|
<dt>Sessão/Alert ID</dt><dd><code>${esc(t.session_id || '—')}</code></dd>
|
|
${t.wizard_ticket_id ? `<dt>Chamado wizard</dt><dd><code class="session-hash">${esc(t.wizard_ticket_id)}</code></dd>` : ''}
|
|
${t.wizard_client_note ? `<dt>Nota cliente</dt><dd>${esc(t.wizard_client_note)}</dd>` : ''}
|
|
<dt>Verificado</dt><dd>${t.account_verified ? 'Sim' : 'Não'}</dd>
|
|
<dt>Revisão</dt><dd>${t.needs_review ? 'Necessária' : 'Não'}</dd>
|
|
<dt>Criado</dt><dd>${fmtDate(t.created_at)}</dd>
|
|
</dl>
|
|
${sessionId && t.source === 'vm112-onboard' ? assistActionsHtml(sessionId, {
|
|
can_escalate: assistMeta?.can_escalate,
|
|
assist_status: assistMeta?.assist_status || assistMeta?.ticket_status,
|
|
ticket_status: assistMeta?.ticket_status || t.status,
|
|
client_paused: assistMeta?.ticket?.client_paused ?? t.client_paused,
|
|
assisted_by: assistMeta?.assisted_by,
|
|
actions: assistMeta?.actions,
|
|
}, assistMeta?._console || {}) : ''}
|
|
${carbonioBlock ? `<div id="ticket-carbonio-block">${carbonioBlockPanelHtml(carbonioBlock)}</div>` : ''}
|
|
<div class="actions">
|
|
${typeof canPatchTickets === 'function' && canPatchTickets()
|
|
? (closeStatuses.includes(t.status)
|
|
? '<button type="button" class="btn btn-primary" data-action="close">Fechar ticket</button>'
|
|
: '<button type="button" class="btn btn-ghost" data-action="open">Reabrir ticket</button>')
|
|
: ''}
|
|
</div>
|
|
<details class="ticket-payload-details">
|
|
<summary>Payload técnico</summary>
|
|
<pre class="raw">${esc(JSON.stringify(t.payload, null, 2))}</pre>
|
|
</details>
|
|
</div>`;
|
|
},
|
|
|
|
livePaneHtml(sessionId) {
|
|
const enriched = window.TicketsWorkspace?.context?.liveBySession?.[sessionId];
|
|
return `
|
|
<div class="ticket-detail-pane" data-ticket-pane="live"${this.activeTab !== 'live' ? ' hidden' : ''}>
|
|
${enriched ? `
|
|
<div class="ticket-live-presence-card">
|
|
<span class="ticket-signal ticket-signal--live is-live"><i aria-hidden="true"></i>LIVE</span>
|
|
<dl class="kv kv--compact">
|
|
<dt>Path</dt><dd><code>${esc(enriched.path || '—')}</code></dd>
|
|
<dt>Passo wizard</dt><dd>${esc(enriched.wizard_step || '—')}</dd>
|
|
<dt>IP</dt><dd>${esc(enriched.ip || '—')}</dd>
|
|
<dt>Último sinal</dt><dd>${fmtDate(enriched.last_seen_at || enriched.updated_at)}</dd>
|
|
</dl>
|
|
</div>` : '<p class="ticket-meta">Cliente offline neste momento.</p>'}
|
|
<div id="ticket-detail-live-trail"></div>
|
|
<button type="button" class="btn btn-ghost btn-sm" data-open-live-trail="${esc(sessionId)}">Trail completo (modal)</button>
|
|
</div>`;
|
|
},
|
|
|
|
funilPaneHtml(timeline, timing) {
|
|
return `
|
|
<div class="ticket-detail-pane" data-ticket-pane="funil"${this.activeTab !== 'funil' ? ' hidden' : ''}>
|
|
${timeline?.length
|
|
? `${phaseTimingCardHtml(timing, timeline)}${timelineHtml(timeline, timing, { compact: false })}`
|
|
: '<p class="ticket-meta">Sem eventos de funil para esta sessão.</p>'}
|
|
</div>`;
|
|
},
|
|
|
|
bindTabs(detailEl) {
|
|
detailEl.querySelectorAll('[data-ticket-tab]').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
this.activeTab = btn.dataset.ticketTab;
|
|
detailEl.querySelectorAll('[data-ticket-tab]').forEach((b) => {
|
|
b.classList.toggle('active', b === btn);
|
|
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
|
|
});
|
|
detailEl.querySelectorAll('[data-ticket-pane]').forEach((pane) => {
|
|
pane.hidden = pane.dataset.ticketPane !== this.activeTab;
|
|
});
|
|
if (this.activeTab === 'live') {
|
|
const sid = detailEl.dataset.sessionId;
|
|
const trail = detailEl.querySelector('#ticket-detail-live-trail');
|
|
if (sid && trail && window.DeskLive?.renderNavigationTab) {
|
|
DeskLive.renderNavigationTab(sid, trail);
|
|
}
|
|
}
|
|
if (this.activeTab === 'espelho') {
|
|
const sid = detailEl.dataset.sessionId;
|
|
if (sid) this.startMirrorPoll(detailEl, sid);
|
|
} else {
|
|
this.stopMirrorPoll();
|
|
}
|
|
if (this.activeTab === 'funil') bindLiveTimingClock(detailEl);
|
|
});
|
|
});
|
|
},
|
|
|
|
bindNextActions(detailEl, sessionId) {
|
|
detailEl.querySelector('[data-next-action="takeover"]')?.addEventListener('click', async (e) => {
|
|
const btn = e.currentTarget;
|
|
btn.disabled = true;
|
|
try {
|
|
await runAssistAction('takeover', sessionId);
|
|
await renderTickets();
|
|
} catch (err) {
|
|
alert(err.message || 'Falha ao assumir sessão');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
detailEl.querySelector('[data-next-action="resume-wizard"]')?.addEventListener('click', async (e) => {
|
|
const btn = e.currentTarget;
|
|
btn.disabled = true;
|
|
try {
|
|
await runAssistAction('resume-wizard', sessionId);
|
|
await renderTickets();
|
|
} catch (err) {
|
|
alert(err.message || 'Falha ao reabrir wizard ASM');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
detailEl.querySelector('[data-next-action="escalate"]')?.addEventListener('click', async (e) => {
|
|
const btn = e.currentTarget;
|
|
btn.disabled = true;
|
|
try {
|
|
await runAssistAction('escalate', sessionId);
|
|
await renderTickets();
|
|
} catch (err) {
|
|
alert(err.message || 'Falha ao escalar');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
detailEl.querySelector('[data-next-action="scroll-carbonio"]')?.addEventListener('click', () => {
|
|
this.activeTab = 'resumo';
|
|
detailEl.querySelector('[data-ticket-tab="resumo"]')?.click();
|
|
detailEl.querySelector('#ticket-carbonio-block')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
detailEl.querySelector('[data-open-live-trail]')?.addEventListener('click', (e) => {
|
|
if (window.DeskLive?.openTrail) DeskLive.openTrail(e.currentTarget.dataset.openLiveTrail);
|
|
});
|
|
},
|
|
|
|
async render(ticketId, detailEl) {
|
|
if (!ticketId || !detailEl) return;
|
|
this.stopMirrorPoll();
|
|
const isFreshTicket = this.lastTicketId !== ticketId;
|
|
if (isFreshTicket) {
|
|
this.activeTab = 'resumo';
|
|
this.lastTicketId = ticketId;
|
|
}
|
|
detailEl.innerHTML = '<div class="card detail-panel"><p class="loading">Carregando…</p></div>';
|
|
try {
|
|
const t = await api(`/v1/desk/tickets/${ticketId}`);
|
|
const sessionId = t.session_id || state.selectedSessionId;
|
|
const assistMeta = sessionId && t.source === 'vm112-onboard'
|
|
? await loadAssistMeta(sessionId)
|
|
: null;
|
|
if (sessionId) state.selectedSessionId = sessionId;
|
|
|
|
let carbonioBlock = null;
|
|
if (t.source === 'vm112-onboard' && window.DeskModules?.isEnabled('carbonio-release')) {
|
|
try {
|
|
const byTicket = await api(`/v1/carbonio-blocks?ticket_id=${t.id}&status=pending&limit=1`);
|
|
carbonioBlock = byTicket.blocks?.[0] || null;
|
|
if (!carbonioBlock && sessionId) {
|
|
const bySession = await api(`/v1/carbonio-blocks?session_id=${encodeURIComponent(sessionId)}&status=pending&limit=1`);
|
|
carbonioBlock = bySession.blocks?.[0] || null;
|
|
}
|
|
} catch {
|
|
carbonioBlock = null;
|
|
}
|
|
}
|
|
|
|
const timeline = assistMeta?.timeline?.length
|
|
? assistMeta.timeline
|
|
: (t.timeline || t.related_events || []);
|
|
const timing = assistMeta?.timing || t.timing;
|
|
const hasLive = Boolean(sessionId && t.source === 'vm112-onboard' && window.DeskLive?.enabled());
|
|
const hasEspelho = Boolean(sessionId && t.source === 'vm112-onboard');
|
|
const hasFunil = timeline.length > 0;
|
|
const assistStatus = normalizeAssistStatus(assistMeta?.assist_status || assistMeta?.ticket_status || t.status);
|
|
if (isFreshTicket && hasEspelho && hasLive && assistStatus !== 'assisting') {
|
|
this.activeTab = 'espelho';
|
|
}
|
|
const nextAction = this.computeNextAction(t, assistMeta, carbonioBlock);
|
|
|
|
detailEl.innerHTML = `
|
|
<div class="card detail-panel ticket-detail-shell" data-session-id="${esc(sessionId || '')}">
|
|
<div class="ticket-detail-header">
|
|
<div>
|
|
<h3 style="margin:0">Ticket #${t.id}${t.wizard_ticket_id ? ` · <code class="session-hash">${esc(t.wizard_ticket_id)}</code>` : ''}</h3>
|
|
<p class="ticket-meta">${esc(t.domain || t.subject || t.agent || '')}</p>
|
|
</div>
|
|
<span class="badge ${t.status}">${esc(statusLabel(t.status))}</span>
|
|
</div>
|
|
${this.nextActionHtml(nextAction)}
|
|
${this.tabsHtml({ t, sessionId, hasLive, hasFunil, hasEspelho })}
|
|
${this.resumoHtml(t, { sessionId, assistMeta, carbonioBlock, timeline, timing })}
|
|
${hasEspelho ? this.espelhoPaneHtml() : ''}
|
|
${hasLive ? this.livePaneHtml(sessionId) : ''}
|
|
${hasFunil ? this.funilPaneHtml(timeline, timing) : ''}
|
|
</div>`;
|
|
|
|
this.bindTabs(detailEl);
|
|
this.bindNextActions(detailEl, sessionId);
|
|
if (sessionId && t.source === 'vm112-onboard') bindAssistActions(detailEl, sessionId);
|
|
bindCarbonioResolveForms(detailEl);
|
|
bindLiveTimingClock(detailEl);
|
|
if (this.activeTab === 'espelho' && sessionId) {
|
|
this.startMirrorPoll(detailEl, sessionId);
|
|
}
|
|
if (this.activeTab === 'live' && hasLive) {
|
|
const trail = detailEl.querySelector('#ticket-detail-live-trail');
|
|
if (trail) await DeskLive.renderNavigationTab(sessionId, trail);
|
|
}
|
|
detailEl.querySelector('[data-action="close"]')?.addEventListener('click', () => updateTicketStatus('closed'));
|
|
detailEl.querySelector('[data-action="open"]')?.addEventListener('click', () => updateTicketStatus('open'));
|
|
} catch (e) {
|
|
detailEl.innerHTML = `<div class="card"><p class="loading">Erro: ${esc(e.message)}</p></div>`;
|
|
}
|
|
},
|
|
};
|
|
|
|
window.TicketsDetailPanel = TicketsDetailPanel;
|