/**
* 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 '
Carregando espelho do wizard…
';
}
if (observer.is_assisting) {
return 'ASM ativo — o cliente está pausado. Use Reabrir wizard ASM para continuar no modo técnico.
';
}
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 Ajuda do Suporte aberto');
if (modals.dns) modalLines.push('Modal DNS aberto');
if (modals.dns_advanced) modalLines.push('Modal DNS avançado aberto');
const activity = (observer.activity_log || []).slice(-12).map((e) => {
const lvl = (e.level || 'info').toLowerCase();
return ` ${esc(e.message || e.msg || '')}`;
}).join('');
const infraPending = (observer.infra_status?.steps || []).filter((s) => !s.ok).slice(0, 4)
.map((s) => esc(s.label || s.id)).join(' · ');
return `
O técnico lê aqui o mesmo contexto do cliente antes de assumir a sessão.
Cliente no wizard
${observer.presence?.wizard_step || step !== '—' ? `${esc(step)}` : ''}
- Passo
- ${esc(step)}
- Domínio
- ${esc(domain)}
${observer.wizard_ticket_id ? `- Chamado wizard
${esc(observer.wizard_ticket_id)} ` : ''}
${observer.client_note ? `- Nota do cliente
- ${esc(observer.client_note)}
` : ''}
${err ? `
Erro na tela do cliente${esc(err)}
` : ''}
${modalLines.length ? `
${modalLines.join(' · ')}
` : ''}
${infraPending ? `
Infra pendente: ${infraPending}
` : ''}
Terminal / activity log (cliente)
${activity || '- Sem linhas recentes
'}
Atualizado ${fmtDate(observer.presence?.last_seen_at || new Date().toISOString())}
`;
},
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 = `Espelho indisponível: ${esc(e.message)}
`;
}
},
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 `
Carregando espelho do wizard…
`;
},
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'
? ''
: action.cta === 'resume-wizard'
? ''
: action.cta === 'escalate'
? ''
: action.cta === 'scroll-carbonio'
? ''
: '';
return `
${action.icon}
${esc(action.title)}
${esc(action.text)}
${cta ? `
${cta}
` : ''}
`;
},
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 `
`;
},
resumoHtml(t, { sessionId, assistMeta, carbonioBlock, timeline, timing }) {
const closeStatuses = ['open', 'escalated', 'assisting', 'resolved'];
return `
- Origem
- ${sourceBadge(t.source)}
- Domínio/Agente
- ${esc(t.domain || t.agent || '—')}
- Email
- ${esc(t.email || '—')}
${typeof ticketFunnelKvHtml === 'function' ? ticketFunnelKvHtml(t) : `- Evento
- ${esc(t.event || '—')}
`}
${t.assigned_to ? `- Atribuído
- ${esc(t.assigned_to)}
` : ''}
${t.assisted_by ? `- Assistido por
- ${esc(t.assisted_by)}
` : ''}
${t.client_paused ? '- Cliente
- pausado
' : ''}
${t.ready_for_ops ? '- Ops
- ready for ops
' : ''}
${t.severity != null ? `- Severidade
- ${severityBadge(t.severity)}
` : ''}
${t.rule_id ? `- Regra
- ${esc(t.rule_id)}
` : ''}
${t.description ? `- Descrição
- ${esc(t.description)}
` : ''}
${t.desk_message ? `- Nota
- ${esc(t.desk_message)}
` : ''}
${t.registration_role ? `- Perfil
- ${esc(roleLabel(t.registration_role))}
` : ''}
${t.activation_url ? `- Ativar conta
- Abrir link de ativação
` : ''}
- Sessão/Alert ID
${esc(t.session_id || '—')}
${t.wizard_ticket_id ? `- Chamado wizard
${esc(t.wizard_ticket_id)} ` : ''}
${t.wizard_client_note ? `- Nota cliente
- ${esc(t.wizard_client_note)}
` : ''}
- Verificado
- ${t.account_verified ? 'Sim' : 'Não'}
- Revisão
- ${t.needs_review ? 'Necessária' : 'Não'}
- Criado
- ${fmtDate(t.created_at)}
${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 ? `
${carbonioBlockPanelHtml(carbonioBlock)}
` : ''}
${typeof canPatchTickets === 'function' && canPatchTickets()
? (closeStatuses.includes(t.status)
? ''
: '')
: ''}
Payload técnico
${esc(JSON.stringify(t.payload, null, 2))}
`;
},
livePaneHtml(sessionId) {
const enriched = window.TicketsWorkspace?.context?.liveBySession?.[sessionId];
return `
${enriched ? `
LIVE
- Path
${esc(enriched.path || '—')}
- Passo wizard
- ${esc(enriched.wizard_step || '—')}
- IP
- ${esc(enriched.ip || '—')}
- Último sinal
- ${fmtDate(enriched.last_seen_at || enriched.updated_at)}
` : '
Cliente offline neste momento.
'}
`;
},
funilPaneHtml(timeline, timing) {
return `
${timeline?.length
? `${phaseTimingCardHtml(timing, timeline)}${timelineHtml(timeline, timing, { compact: false })}`
: '
Sem eventos de funil para esta sessão.
'}
`;
},
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 = '';
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 = `
${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) : ''}
`;
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 = ``;
}
},
};
window.TicketsDetailPanel = TicketsDetailPanel;