/** * 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 ? `` : ''} ${modalLines.length ? `
    ${modalLines.join(' · ')}
    ` : ''} ${infraPending ? `

    Infra pendente: ${infraPending}

    ` : ''}
    Terminal / activity log (cliente)
      ${activity || '
    1. Sem linhas recentes
    2. '}

    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 `
    ${esc(action.title)} ${esc(action.text)}
    ${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 = '

    Carregando…

    '; 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 = `

    Ticket #${t.id}${t.wizard_ticket_id ? ` · ${esc(t.wizard_ticket_id)}` : ''}

    ${esc(t.domain || t.subject || t.agent || '')}

    ${esc(statusLabel(t.status))}
    ${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 = `

    Erro: ${esc(e.message)}

    `; } }, }; window.TicketsDetailPanel = TicketsDetailPanel;