Files
2026-09-23 12:29:08 -03:00

1493 lines
116 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<panel-form xmlns="http://www.davinti.com.br/vitruvio/form/panel"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form/panel https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-panel-form.xsd">
<form formKey="dashboard_tecnicos" width="100%" height="100%">
<name>Dashboard Técnicos</name>
<description>Dashboard de produtividade dos técnicos com ranking, SLA e detalhamento por OS.</description>
<afterFormRenderScript language="JavaScript">
<![CDATA[
function run(){
// Inicializa o agente Pietro na sidebar direita
try {
var dashLib = libService.loadScript('dashboard_ia');
var login = String(engine.getLoggedUser().getLogin());
var prefs = dashLib.loadPreferences(login);
var savedTheme = prefs.dark_mode ? 'dark' : 'light';
// Garante que o chatBar tem o tema correto antes de abrir (afterFormRender é o último a rodar)
var _cbAft = engine.getLayout('chatBar');
if (_cbAft) {
var _cbAftComp = _cbAft.getRootComposition();
if (savedTheme === 'light') {
_cbAftComp.removeStyleName('dash-chatbar-dark');
_cbAftComp.addStyleName('dash-chatbar-light');
} else {
_cbAftComp.removeStyleName('dash-chatbar-light');
_cbAftComp.addStyleName('dash-chatbar-dark');
}
}
var chatAgente = libService.loadScript('IA_chat_agente_ia');
engine.setGlobalVariable('chatAgente', chatAgente);
// Guardado à parte (não só passado pro abrir()) pra o botão do chat nativo
// (btnChatFabMobile) reaproveitar exatamente o mesmo contexto/prompt do chat
// web, sem duplicar esse texto em dois lugares.
var chatConfig = {
contexto: 'dashboard-tecnicos',
titulo: 'Pietro',
nomeAgente: 'Pietro',
tema: savedTheme,
saudacao: 'Olá! Posso ajudar com análise da produtividade dos técnicos, SLA e Ordens de Serviço.',
prompt: {
persona: 'Você é Pietro, assistente de análise de dados do Dashboard Técnicos (produtividade dos técnicos, cumprimento de SLA e Ordens de Serviço).',
tarefa: 'Ajude o usuário a interpretar os KPIs, gráficos e a movimentação exibidos no painel agora. Use SEMPRE os dados de "secoes" em dados_contexto como sua única fonte de verdade — cada seção já traz o título exato exibido na tela, o tipo (kpi/grafico/tabela) e os valores atuais do período/filtro selecionado.',
regras: 'Responda apenas sobre produtividade de técnicos, SLA e Ordens de Serviço (OS), e apenas com os dados de "secoes" da view ATUAL (dados_contexto.view) — nunca misture com dados de outra view do painel (dashboard vs movimentação) nem com dados de mensagens anteriores desta conversa, mesmo que pareçam relacionados: o dados_contexto mais recente enviado é a única fonte válida, qualquer coisa anterior é considerada desatualizada. Se a pergunta pedir algo que não está no dados_contexto atual (outra view, outro período, outro filtro, um detalhe não carregado na tela), diga claramente que essa informação não está disponível no contexto atual e oriente o usuário a mudar view/filtro/período no painel. Nunca estime, calcule por fora do contexto fornecido ou invente um valor.',
saida: 'Seja direto e objetivo. Ao citar um dado, use o mesmo título de seção exibido na tela.'
}
};
engine.setGlobalVariable('nativeChatConfig', chatConfig);
chatAgente.abrir(chatConfig);
chatAgente.fechar();
} catch(e) {}
}
]]>
</afterFormRenderScript>
<initScript language="JavaScript">
<![CDATA[
var addCss = function(page) {
var style =
".dash-wrapper { background:#0f1923; padding:10px 12px; font-family:Arial,Helvetica,sans-serif; box-sizing:border-box; width:100%; height:100%; overflow-y:auto; overflow-x:hidden; } " +
".dash-kpi { background:#1a2733; border-radius:6px; padding:6px 10px; flex:0 0 auto; min-width:110px; display:flex; flex-direction:column; justify-content:center; } " +
".dash-kpi-label { color:#8899aa; font-size:10px; margin-bottom:2px; text-transform:uppercase; letter-spacing:1px; } " +
".dash-kpi-value { font-size:18px; font-weight:bold; color:#fff; } " +
".dash-chart-box { background:#1a2733; border-radius:6px; padding:8px 10px; margin-bottom:8px; } " +
".dash-chart-title { color:#8899aa; font-size:11px; font-weight:bold; text-transform:uppercase; letter-spacing:1px; margin-bottom:6px; display:flex; align-items:center; justify-content:space-between; } " +
".dtech-legend { background:#1a2733; border-left:3px solid #4a9edd; padding:4px 10px; font-size:10px; color:#8899aa; margin-bottom:8px; border-radius:0 4px 4px 0; } " +
".theme-light .dtech-legend { background:#eff6ff; border-left-color:#2563eb; color:#2563eb; } " +
"table.dtech-tec-table { border-collapse:collapse; width:100%; font-size:11px; } " +
"table.dtech-tec-table th { background:#243447; color:#8899aa; padding:5px 8px; text-align:left; white-space:nowrap; position:sticky; top:0; z-index:1; } " +
"table.dtech-tec-table th.sortable { cursor:pointer; user-select:none; } " +
"table.dtech-tec-table th.sortable:hover { background:#2a3a4a; } " +
"table.dtech-tec-table td { padding:4px 8px; border-bottom:1px solid #162030; color:#ccc; vertical-align:middle; } " +
"table.dtech-tec-table tr:hover td { background:#1e2d3d; } " +
".theme-light table.dtech-tec-table th { background:#f8fafc; color:#64748b; border-bottom:1px solid #e2e8f0; position:sticky; top:0; z-index:1; } " +
".theme-light table.dtech-tec-table th.sortable:hover { background:#f1f5f9; } " +
".theme-light table.dtech-tec-table td { color:#334155; border-bottom-color:#e2e8f0; } " +
".theme-light table.dtech-tec-table tr:hover td { background:#f1f5f9; } " +
"table.mov-table { border-collapse:collapse; width:100%; font-size:11px; } " +
"table.mov-table th { background:#243447; color:#8899aa; padding:5px 8px; text-align:left; white-space:nowrap; } " +
"table.mov-table td { padding:5px 8px; border-bottom:1px solid #162030; color:#ccc; white-space:nowrap; vertical-align:middle; } " +
"table.mov-table tr.dtech-mov-row { cursor:pointer; } " +
"table.mov-table tr.dtech-mov-row:hover td { background:#1e2d3d !important; } " +
"table.mov-table tr.dtech-mov-detail td { cursor:default; background:#0a1525; white-space:normal; } " +
"table.mov-table tr.dtech-mov-detail:hover td { background:#0a1525 !important; } " +
".theme-light table.mov-table th { background:#f8fafc; color:#64748b; border-bottom:1px solid #e2e8f0; } " +
".theme-light table.mov-table td { color:#334155; border-bottom-color:#e2e8f0; } " +
".theme-light table.mov-table tr.dtech-mov-row:hover td { background:#f1f5f9 !important; } " +
".theme-light table.mov-table tr.dtech-mov-detail td { background:#f8fafc; } " +
".theme-light table.mov-table tr.dtech-mov-detail:hover td { background:#f8fafc !important; } " +
".dtech-badge-ok { display:inline-block; background:#0d2d1a; color:#2ecc71; border:1px solid #2ecc71; padding:1px 7px; border-radius:10px; font-size:10px; font-weight:bold; } " +
".dtech-badge-fora { display:inline-block; background:#2d0d0d; color:#e74c3c; border:1px solid #e74c3c; padding:1px 7px; border-radius:10px; font-size:10px; font-weight:bold; } " +
".theme-light .dtech-badge-ok { background:#dcfce7; color:#16a34a; border-color:#16a34a; } " +
".theme-light .dtech-badge-fora { background:#fee2e2; color:#dc2626; border-color:#dc2626; } " +
".dash-wrapper .donut-hole { fill:#1a2733; } " +
".theme-light .donut-hole { fill:#ffffff; } " +
// Sidebar filterBar
".dash-filterbar-dark { background:#0d1921 !important; border-right:1px solid #1a2a38; } " +
".dash-filterbar-light { background:#f1f5f9 !important; border-right:1px solid #e2e8f0; } " +
".dash-filterbar-dark .v-caption { color:#8899aa !important; font-size:12px !important; } " +
".dash-filterbar-light .v-caption { color:#64748b !important; font-size:12px !important; } " +
// Chat sidebar (direita) — Pietro
".dash-chatbar-dark { background:#0d1921 !important; border-left:1px solid #1a2a38 !important; } " +
".dash-chatbar-light { background:#f1f5f9 !important; border-left:1px solid #e2e8f0 !important; } " +
// Botão flutuante (FAB) do chat Pietro no mobile — fixo no canto inferior direito,
// fora do fluxo do layout (mesmo padrão de position:fixed do .preview-exit-btn abaixo).
".dash-chat-fab { position:fixed !important; right:16px !important; bottom:20px !important; width:56px !important; height:56px !important; min-width:0 !important; border-radius:50% !important; overflow:hidden !important; border:none !important; background:#4a9edd !important; box-shadow:0 4px 16px rgba(0,0,0,.4) !important; z-index:9000 !important; padding:0 !important; } " +
".dash-chat-fab .v-button-wrap { border-radius:50% !important; background:transparent !important; } " +
".dash-chat-fab .v-button-caption { font-size:24px !important; line-height:56px !important; color:#fff !important; } " +
// Preview frame + exit button
".preview-mobile-frame { margin:0 auto !important; border:2px solid #4a9edd !important; border-radius:12px !important; overflow:hidden !important; box-shadow:0 0 40px rgba(74,158,221,0.25) !important; } " +
".preview-exit-btn { position:fixed !important; top:10px !important; right:12px !important; z-index:99999 !important; background:#e74c3c !important; border:none !important; border-radius:6px !important; padding:7px 16px !important; font-weight:bold !important; box-shadow:0 2px 12px rgba(0,0,0,0.4) !important; cursor:pointer !important; } " +
".preview-exit-btn .v-button-caption { color:#fff !important; font-size:12px !important; } " +
// Mobile: cards de técnicos
".tec-card { background:#1a2733; border-radius:6px; padding:12px 16px; margin-bottom:8px; } " +
".tec-card-rank { display:inline-block; background:#243447; color:#8899aa; font-size:10px; font-weight:bold; padding:1px 6px; border-radius:3px; margin-right:6px; } " +
".tec-card-nome { font-weight:bold; color:#fff; font-size:15px; margin-bottom:6px; } " +
".tec-card-meta { font-size:13px; color:#8899aa; margin-bottom:8px; } " +
".tec-card-values { display:flex; flex-wrap:wrap; gap:6px; } " +
".tec-card-val { flex:1; min-width:80px; background:#0f1923; border-radius:4px; padding:6px 8px; } " +
".tec-card-lbl { display:block; font-size:12px; color:#8899aa; margin-bottom:2px; } " +
// Mobile: cards de movimentação
".mov-card { background:#1a2733; border-radius:6px; padding:12px 16px; margin-bottom:8px; cursor:pointer; } " +
".mov-card-chamado { font-weight:bold; color:#4a9edd; font-size:15px; margin-bottom:4px; } " +
".mov-card-tecnico { color:#fff; font-size:13px; margin-bottom:4px; } " +
".mov-card-meta { font-size:13px; color:#8899aa; margin-bottom:8px; } " +
".mov-card-values { display:flex; flex-wrap:wrap; gap:6px; } " +
".mov-card-val { flex:1; min-width:80px; background:#0f1923; border-radius:4px; padding:6px 8px; } " +
".mov-card-lbl { display:block; font-size:12px; color:#8899aa; margin-bottom:2px; } " +
".mov-card-detail { border-top:1px solid #243447; margin-top:8px; padding-top:8px; font-size:12px; display:flex; flex-wrap:wrap; gap:10px; } " +
".mov-card-detail-item { min-width:120px; } " +
".mov-card-detail-lbl { display:block; font-size:10px; color:#556677; text-transform:uppercase; margin-bottom:2px; } " +
".theme-light .tec-card { background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .tec-card-nome { color:#1e293b; } " +
".theme-light .tec-card-meta { color:#64748b; } " +
".theme-light .tec-card-lbl { color:#64748b; } " +
".theme-light .tec-card-val { background:#f8fafc; } " +
".theme-light .mov-card { background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .mov-card-chamado { color:#2563eb; } " +
".theme-light .mov-card-tecnico { color:#1e293b; } " +
".theme-light .mov-card-meta { color:#64748b; } " +
".theme-light .mov-card-lbl { color:#64748b; } " +
".theme-light .mov-card-val { background:#f8fafc; } " +
".theme-light .mov-card-detail { border-top-color:#e2e8f0; color:#334155; } " +
".theme-light .mov-card-detail-lbl { color:#94a3b8; } " +
// Mobile: layout e fontes
".dash-wrapper.mobile-view { -webkit-overflow-scrolling:touch; overscroll-behavior:contain; touch-action:pan-y; } " +
".mobile-topbar { position:-webkit-sticky !important; position:sticky !important; top:0 !important; z-index:60 !important; flex-wrap:wrap; padding:8px !important; } " +
".mobile-topbar .v-button { min-width:0 !important; } " +
".mobile-view .tec-mob-scroll { max-height:420px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " +
".mobile-view .hbar-scroll { max-height:280px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; } " +
".mobile-view .dash-kpi-label { font-size:13px !important; } " +
".mobile-view .dash-kpi-value { font-size:20px !important; } " +
".mobile-view .dash-chart-title { font-size:13px !important; } " +
".mobile-view .dtech-legend { font-size:13px !important; } " +
// Barra de pesquisa mobile (movimentação)
".mob-search-bar { display:flex; flex-direction:column; gap:6px; margin-bottom:10px; padding:8px 10px; background:#1a2733; border-radius:6px; } " +
".mob-search-input { width:100%; box-sizing:border-box; background:#0f1923; border:1px solid #243447; color:#ccc; font-size:14px; padding:9px 10px; border-radius:5px; outline:none; } " +
".mob-search-row { display:flex; gap:6px; align-items:center; } " +
".mob-search-sel { flex:1; min-width:0; background:#0f1923; border:1px solid #243447; color:#ccc; font-size:13px; padding:7px 6px; border-radius:5px; outline:none; } " +
".mob-search-count { font-size:12px; color:#8899aa; white-space:nowrap; flex-shrink:0; } " +
".theme-light .mob-search-bar { background:#f8fafc; border:1px solid #e2e8f0; } " +
".theme-light .mob-search-input { background:#fff; border-color:#cbd5e1; color:#334155; } " +
".theme-light .mob-search-sel { background:#fff; border-color:#cbd5e1; color:#334155; } " +
// Cards compactos de técnicos (mobile)
".tec-mob-card { background:#1a2733; border-radius:6px; padding:10px 12px; margin-bottom:6px; } " +
".tec-mob-row1 { display:flex; align-items:center; gap:6px; margin-bottom:5px; } " +
".tec-mob-rank { background:#243447; color:#8899aa; font-size:11px; font-weight:bold; padding:2px 5px; border-radius:3px; flex-shrink:0; min-width:26px; text-align:center; } " +
".tec-mob-nome { flex:1; font-weight:bold; color:#fff; font-size:15px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } " +
".tec-mob-total { font-size:22px; font-weight:bold; color:#4a9edd; flex-shrink:0; line-height:1; } " +
".tec-mob-sla { display:flex; align-items:center; gap:8px; margin-bottom:3px; } " +
".tec-mob-sla-bar { flex:1; height:7px; background:#243447; border-radius:3px; overflow:hidden; } " +
".tec-mob-sla-info { font-size:13px; color:#8899aa; white-space:nowrap; flex-shrink:0; } " +
".tec-mob-cli { font-size:13px; color:#556677; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } " +
".tec-sort-btn { background:#243447; border:none; color:#8899aa; font-size:12px; padding:5px 12px; border-radius:4px; cursor:pointer; } " +
".tec-sort-btn.active { background:#1e3a55; color:#4a9edd; } " +
".theme-light .tec-mob-card { background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .tec-mob-rank { background:#f1f5f9; } " +
".theme-light .tec-mob-nome { color:#1e293b; } " +
".theme-light .tec-mob-sla-info { color:#64748b; } " +
".theme-light .tec-mob-sla-bar { background:#e2e8f0; } " +
".theme-light .tec-mob-cli { color:#94a3b8; } " +
".theme-light .tec-sort-btn { background:#f1f5f9; color:#64748b; } " +
".theme-light .tec-sort-btn.active { background:#dbeafe; color:#2563eb; } ";
page.getStyles().add(style);
};
function run() {
var dashLib = libService.loadScript('dashboard_ia');
var login = String(engine.getLoggedUser().getLogin());
engine.setGlobalVariable('dashLib', dashLib);
engine.setGlobalVariable('userLogin', login);
var page = Packages.com.vaadin.ui.UI.getCurrent().getPage();
dashLib.addSharedCss(page);
addCss(page);
var _topBar = engine.getLayout('topBar');
if (_topBar) { _topBar.getRootComposition().addStyleName('dash-topbar'); }
var _btnFabCls = engine.getWidgetController('btnChatFabMobile');
if (_btnFabCls) { _btnFabCls.getButton().addStyleName('dash-chat-fab'); }
if (engine.isGlobalVariableSet('_mobileRender')) {
engine.setGlobalVariable('isMobile', true);
var _rootLayout = engine.getLayout('rootLayout');
if (_rootLayout) {
var _rootLayoutComp = _rootLayout.getRootComposition();
// 700px é só o placeholder inicial — o server não sabe a altura da tela do
// cliente. O script abaixo troca por um valor proporcional a window.screen.height
// assim que a página carrega, ANTES do colapso de renderHtml.
_rootLayoutComp.setHeight("700px");
_rootLayoutComp.addStyleName('mobile-root-frame');
page.getJavaScript().execute(
"(function(){" +
"var el=document.querySelector('.mobile-root-frame');" +
"if(!el){return;}" +
"var sh=window.screen.height;" +
"var byMargin=sh-220;" +
"var byRatio=Math.round(sh*0.80);" +
"var h=Math.max(320,Math.min(byMargin,byRatio));" +
"el.style.height=h+'px';" +
"})();"
);
}
if (_topBar) { _topBar.getRootComposition().addStyleName('mobile-topbar'); }
var _filterBar = engine.getLayout('filterBar');
if (_filterBar) { _filterBar.getRootComposition().setVisible(false); }
var _btnThemeMob = engine.getWidgetController('btnThemeMob');
if (_btnThemeMob) { _btnThemeMob.getButton().setVisible(true); }
// Oculta btnTheme do topBar — no mobile só btnThemeMob (dentro da filterBar) é visível.
var _btnThemeTb = engine.getWidgetController('btnTheme');
if (_btnThemeTb) { _btnThemeTb.getButton().setVisible(false); }
// Pietro no mobile: a sidebar web (chatBar) some, e um botão flutuante (FAB)
// abre o chat NATIVO do app (React Native), não a sidebar embutida na WebView —
// ver onClickScript de btnChatFabMobile, que despacha postMessage.
var _btnTogChatMob = engine.getWidgetController('btnToggleChat');
if (_btnTogChatMob) { _btnTogChatMob.getButton().setVisible(false); }
var _btnFabMob = engine.getWidgetController('btnChatFabMobile');
if (_btnFabMob) { _btnFabMob.getButton().setVisible(true); }
}
// Verifica acesso de desenvolvedor (vi_developer)
try {
var _dbLib = libService.loadScript('db');
var _bancoChk = new _dbLib(_dbLib.VITRUVIO_DATASOURCE);
var _devRow = _bancoChk.queryRow(
"SELECT COUNT(*) AS CNT FROM nauth.usuario u " +
"INNER JOIN nauth.usuario_grupo ug ON u.usuario_id = ug.usuario_fk " +
"INNER JOIN nauth.grupo g ON g.grupo_id = ug.grupo_fk " +
"WHERE u.login = :login AND g.sigla = 'vi_developer'",
{ login: login }
);
var _isDev = _devRow && Number(_devRow.CNT) > 0;
engine.setGlobalVariable('isDeveloper', _isDev);
var _btnDevCtrl = engine.getWidgetController('btnDevPreview');
// "&& !_mobileRender": no mobile real o botão de preview não deve aparecer.
if (_btnDevCtrl) { _btnDevCtrl.getButton().setVisible(_isDev && !engine.isGlobalVariableSet('_mobileRender')); }
} catch(e) {}
var cal = java.util.Calendar.getInstance();
var hoje = cal.getTime();
cal.set(java.util.Calendar.DAY_OF_MONTH, 1);
engine.getField('dtIni').setValue(cal.getTime());
engine.getField('dtFim').setValue(hoje);
engine.getField('nmfSla').setValue(3);
// filterBar começa fechado
var _filterBarInit = engine.getLayout('filterBar');
if (_filterBarInit) { _filterBarInit.getRootComposition().setVisible(false); }
var _btnToggle = engine.getWidgetController('btnToggleFiltros');
if (_btnToggle) { _btnToggle.getButton().setCaption('\u25BA Filtros'); }
var prefs = dashLib.loadPreferences(login);
var savedTheme = prefs.dark_mode ? 'dark' : 'light';
engine.setGlobalVariable('theme', savedTheme);
if (_filterBarInit) {
_filterBarInit.getRootComposition().addStyleName(
savedTheme === 'light' ? 'dash-filterbar-light' : 'dash-filterbar-dark'
);
}
dashLib.applyTheme(savedTheme, page, null);
var _btnThInit = engine.getWidgetController('btnTheme');
if (_btnThInit) { _btnThInit.getButton().setCaption(savedTheme === 'dark' ? '\u263D' : '\u2600'); }
var _btnThMobInit = engine.getWidgetController('btnThemeMob');
if (_btnThMobInit) { _btnThMobInit.getButton().setCaption(savedTheme === 'dark' ? '\u263D' : '\u2600'); }
page.getJavaScript().execute(
"window.dtechApplyTheme=function(){" +
"var w=document.getElementById('dtech-main');" +
"if(!w)return;" +
"if(document.body.classList.contains('theme-light-app'))w.classList.add('theme-light');" +
"else w.classList.remove('theme-light');" +
"};" +
"window.dtechTecSort=function(col){" +
"var tbl=document.getElementById('tec-tbl');if(!tbl)return;" +
"var tb=tbl.tBodies[0];if(!tb)return;" +
"var cur=tbl.getAttribute('data-sc'),curDir=parseInt(tbl.getAttribute('data-sd'),10)||1;" +
"var dir=(cur===col)?-curDir:-1;" +
"tbl.setAttribute('data-sc',col);tbl.setAttribute('data-sd',dir);" +
"var rows=Array.prototype.slice.call(tb.rows);" +
"rows.sort(function(a,b){" +
"var va=parseFloat(a.getAttribute('data-'+col))||0;" +
"var vb=parseFloat(b.getAttribute('data-'+col))||0;" +
"return (va-vb)*dir;" +
"});" +
"for(var i=0;i<rows.length;i++){tb.appendChild(rows[i]);rows[i].cells[0].textContent=i+1;}" +
"var ths=tbl.tHead.rows[0].cells;" +
"for(var i=0;i<ths.length;i++){" +
"var lbl=ths[i].getAttribute('data-l');" +
"if(lbl)ths[i].innerHTML=lbl+(ths[i].getAttribute('data-col')===col?(dir>0?' &#9650;':' &#9660;'):'');" +
"}" +
"};" +
"window.movSort=function(c,t){" +
"var tbl=document.getElementById('mov-tbl');if(!tbl)return;" +
"var tb=tbl.tBodies[0];if(!tb)return;" +
"var cur=parseInt(tbl.getAttribute('data-sc'),10),curDir=parseInt(tbl.getAttribute('data-sd'),10)||1;" +
"var dir=(cur===c)?-curDir:1;tbl.setAttribute('data-sc',c);tbl.setAttribute('data-sd',dir);" +
"var r=tb.rows,ps=[],i;for(i=0;i<r.length;i+=2)ps.push([r[i],r[i+1]]);" +
"ps.sort(function(a,b){" +
"var va=a[0].cells[c]?a[0].cells[c].getAttribute('data-v')||'':'';" +
"var vb=b[0].cells[c]?b[0].cells[c].getAttribute('data-v')||'':'';" +
"return(t?(+va||0)-(+vb||0):va.localeCompare(vb,'pt'))*dir;" +
"});" +
"for(i=0;i<ps.length;i++){tb.appendChild(ps[i][0]);if(ps[i][1])tb.appendChild(ps[i][1]);}" +
"var hs=tbl.tHead.rows[0].cells;for(i=0;i<hs.length;i++){var lbl=hs[i].getAttribute('data-l');if(lbl)hs[i].innerHTML=lbl+(i===c?(dir>0?' &#9650;':' &#9660;'):'');}" +
"};" +
"window.movFilter=function(){" +
"var fi=document.getElementById('mov-fi');var q=fi?fi.value.toLowerCase():'';" +
"var stf=document.getElementById('mov-stf');var sti=stf?stf.value.toLowerCase():'';" +
"var sef=document.getElementById('mov-sef');var sei=sef?sef.value.toLowerCase():'';" +
"var tbl=document.getElementById('mov-tbl');if(!tbl)return;" +
"var rows=tbl.tBodies[0].rows,vis=0,i;" +
"for(i=0;i<rows.length;i+=2){" +
"var m=rows[i],d=rows[i+1];" +
"var t0=(m.cells[0]?m.cells[0].getAttribute('data-v')||'':'').toLowerCase();" +
"var t1=(m.cells[1]?m.cells[1].getAttribute('data-v')||'':'').toLowerCase();" +
"var t2=(m.cells[2]?m.cells[2].getAttribute('data-v')||'':'').toLowerCase();" +
"var t3=(m.cells[3]?m.cells[3].getAttribute('data-v')||'':'').toLowerCase();" +
"var t4=(m.cells[4]?m.cells[4].getAttribute('data-v')||'':'').toLowerCase();" +
"var tq=(m.getAttribute('data-equip')||'').toLowerCase();" +
"var ok=(!q||t0.indexOf(q)>=0||t1.indexOf(q)>=0||t2.indexOf(q)>=0||tq.indexOf(q)>=0)" +
"&&(!sti||t3===sti)&&(!sei||t4===sei);" +
"m.style.display=ok?'':'none';if(d)d.style.display='none';if(ok)vis++;" +
"}" +
"var c=document.getElementById('mov-rc');if(c)c.textContent=vis+' registro(s)';" +
"};" +
"window.movExportCSV=function(){" +
"var tbl=document.getElementById('mov-tbl');if(!tbl)return;" +
"var tb=tbl.tBodies[0],i;" +
"var h='N Chamado;Tecnico;Cliente;Tipo Servico;Execucao;Data Abertura;Data Ab Etapa;Data Fech Etapa;Status;SLA Dias;Prazo;Equipamento;Nr Serie;Solicitante;Prev Atendimento;Etapa';" +
"var rows=tb.rows,csv=[h];" +
"for(i=0;i<rows.length;i+=2){" +
"var row=rows[i];if(row.style.display==='none')continue;" +
"var c=row.cells;" +
"var prazo=c[10]?c[10].textContent.trim().replace(/[^A-Za-z ]/g,'').trim():'';" +
"csv.push([" +
"(c[0]?c[0].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[1]?c[1].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[2]?c[2].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[3]?c[3].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[4]?c[4].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[5]?c[5].textContent.trim():'')," +
"(c[6]?c[6].textContent.trim():'')," +
"(c[7]?c[7].textContent.trim():'')," +
"(c[8]?c[8].getAttribute('data-v')||'':'').replace(/;/g,',')," +
"(c[9]?c[9].getAttribute('data-v')||'':'')," +
"prazo," +
"(row.getAttribute('data-equip')||'').replace(/;/g,',')," +
"(row.getAttribute('data-serie')||'').replace(/;/g,',')," +
"(row.getAttribute('data-solic')||'').replace(/;/g,',')," +
"(row.getAttribute('data-prev')||'').replace(/;/g,',')," +
"(row.getAttribute('data-etapa')||'').replace(/;/g,',')" +
"].join(';'));" +
"}" +
"var content='\\uFEFF'+csv.join('\\r\\n');" +
"var blob=new Blob([content],{type:'text/csv;charset=utf-8'});" +
"var url=URL.createObjectURL(blob);" +
"var a=document.createElement('a');a.href=url;a.download='os-tecnicos.csv';" +
"document.body.appendChild(a);a.click();document.body.removeChild(a);" +
"URL.revokeObjectURL(url);" +
"};" +
"window.movFilterCards=function(){" +
"var fi=document.getElementById('mov-mob-fi');" +
"var q=fi?fi.value.toLowerCase():'';" +
"var stf=document.getElementById('mov-mob-stf');" +
"var sti=stf?stf.value.toLowerCase():'';" +
"var sef=document.getElementById('mov-mob-sef');" +
"var sei=sef?sef.value.toLowerCase():'';" +
"var cards=document.querySelectorAll('.mov-card[data-processo]');" +
"var vis=0,i;" +
"for(i=0;i<cards.length;i++){" +
"var c=cards[i];" +
"var tp=(c.getAttribute('data-processo')||'').toLowerCase();" +
"var tt=(c.getAttribute('data-tecnico')||'').toLowerCase();" +
"var tc=(c.getAttribute('data-cliente')||'').toLowerCase();" +
"var te=(c.getAttribute('data-equip')||'').toLowerCase();" +
"var tty=(c.getAttribute('data-tipo')||'').toLowerCase();" +
"var tex=(c.getAttribute('data-exec')||'').toLowerCase();" +
"var ok=(!q||tp.indexOf(q)>=0||tt.indexOf(q)>=0||tc.indexOf(q)>=0||te.indexOf(q)>=0)" +
"&&(!sti||tty===sti)&&(!sei||tex===sei);" +
"c.style.display=ok?'':'none';if(ok)vis++;" +
"}" +
"var rc=document.getElementById('mov-mob-rc');" +
"if(rc)rc.textContent=vis+' reg.';" +
"};" +
"window.tecSortCards=function(col){" +
"var cont=document.getElementById('tec-mob-list');" +
"if(!cont)return;" +
"var cur=cont.getAttribute('data-sc'),curDir=parseInt(cont.getAttribute('data-sd'),10)||1;" +
"var dir=(cur===col)?-curDir:-1;" +
"cont.setAttribute('data-sc',col);cont.setAttribute('data-sd',dir);" +
"var cards=cont.querySelectorAll('.tec-mob-card');" +
"var arr=Array.prototype.slice.call(cards),i;" +
"arr.sort(function(a,b){" +
"var va=parseFloat(a.getAttribute('data-'+col))||0;" +
"var vb=parseFloat(b.getAttribute('data-'+col))||0;" +
"return (va-vb)*dir;" +
"});" +
"for(i=0;i<arr.length;i++){" +
"cont.appendChild(arr[i]);" +
"var rk=arr[i].querySelector('.tec-mob-rank');" +
"if(rk)rk.textContent='#'+(i+1);" +
"}" +
"var bt=document.getElementById('tec-sort-total');" +
"var bs=document.getElementById('tec-sort-sla');" +
"if(bt){bt.innerHTML='Total'+(col==='total'?(dir>0?' &#9650;':' &#9660;'):'');bt.className='tec-sort-btn'+(col==='total'?' active':'');}" +
"if(bs){bs.innerHTML='% Ok'+(col==='pdentro'?(dir>0?' &#9650;':' &#9660;'):'');bs.className='tec-sort-btn'+(col==='pdentro'?' active':'');}" +
"};"
);
// Sincroniza tema do chatBar antes de inicializar o agente Pietro (afterFormRenderScript)
var _chatBarPreInit = engine.getLayout('chatBar');
if (_chatBarPreInit) {
var _cbPreComp = _chatBarPreInit.getRootComposition();
if (savedTheme === 'light') {
_cbPreComp.removeStyleName('dash-chatbar-dark');
_cbPreComp.addStyleName('dash-chatbar-light');
} else {
_cbPreComp.removeStyleName('dash-chatbar-light');
_cbPreComp.addStyleName('dash-chatbar-dark');
}
}
}
]]>
</initScript>
<components>
<VerticalLayout margin="false" spacing="false" width="100%" height="100%" id="rootLayout">
<!-- Barra superior: toggle filtros + nav + spacer + preview + tema -->
<HorizontalLayout id="topBar" spacing="true" margin="true" width="100%">
<ButtonWidget id="btnToggleFiltros" caption="&#x25C4; Filtros">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fb = engine.getLayout('filterBar');
if (!fb) return;
var filterLayout = fb.getRootComposition();
var estaVisivel = filterLayout.isVisible();
filterLayout.setVisible(!estaVisivel);
var btn = engine.getWidgetController('btnToggleFiltros').getButton();
btn.setCaption(estaVisivel ? '\u25BA Filtros' : '\u25C4 Filtros');
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnDash" caption="DASHBOARD">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.setGlobalVariable('currentView', 'dashboard');
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnMov" caption="MOVIMENTAÇÃO">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.setGlobalVariable('currentView', 'movimentacao');
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<Label width="100%" expandRatio="1" />
<ButtonWidget id="btnDevPreview" caption="&#x1F4F1;" description="Preview Mobile (apenas desenvolvedores)" visible="false">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('enterMobilePreview');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnExitPreview" caption="&#x2715; Sair Preview" description="Voltar ao modo desktop" visible="false">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('exitMobilePreview');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnToggleChat" caption="Pietro &#x25C4;" description="Abrir/fechar assistente Pietro">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var cb = engine.getLayout('chatBar');
if (!cb) return;
var comp = cb.getRootComposition();
var visible = comp.isVisible();
comp.setVisible(!visible);
var btn = engine.getWidgetController('btnToggleChat').getButton();
btn.setCaption(visible ? 'Pietro \u25C4' : '\u25BA Pietro');
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnTheme" caption="&#x263D;" description="Alternar tema escuro / claro">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('doToggleTheme');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<!-- FAB do chat Pietro no mobile — fixo no canto inferior direito (ver .dash-chat-fab
em addCss e o bloco _mobileRender do run()); some no desktop. -->
<ButtonWidget id="btnChatFabMobile" caption="&#128172;" description="Abrir assistente Pietro" visible="false">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
// No mobile o chat passou a ser o nativo do app (React Native), não mais a
// sidebar web (chatBar). Reaproveita o MESMO contexto/prompt guardado pelo
// afterFormRenderScript (nativeChatConfig) e o dados_contexto AO VIVO que
// fica atualizado a cada render (atualizarContextoIA) — assim a IA no app
// responde com os mesmos números que estão na tela agora.
var chatConfig = engine.getGlobalVariable('nativeChatConfig') || {};
var st = engine.getGlobalVariable('chatAgenteState');
var dadosContexto = (st && st.config && st.config.dados_contexto) ? st.config.dados_contexto : null;
var config = {
type: 'OpenNativeChat',
contexto: chatConfig.contexto,
titulo: chatConfig.titulo,
nomeAgente: chatConfig.nomeAgente,
saudacao: chatConfig.saudacao,
prompt: chatConfig.prompt,
dados_contexto: dadosContexto
};
var ui = Packages.com.vaadin.ui.UI.getCurrent();
if (ui) {
var payload = JSON.stringify(config);
ui.getPage().getJavaScript().execute(
"if(window.ReactNativeWebView){window.ReactNativeWebView.postMessage(" + JSON.stringify(payload) + ");}"
);
}
}
]]>
</onClickScript>
</ButtonWidget>
</HorizontalLayout>
<!-- Sidebar filtros + área de conteúdo -->
<HorizontalLayout id="mainArea" spacing="false" margin="false" width="100%" height="100%" expandRatio="1">
<VerticalLayout id="filterBar" spacing="true" margin="true" align="TOP_LEFT" width="220px" height="100%">
<!-- Botão tema único para mobile (mesmo comportamento do btnTheme do desktop) -->
<ButtonWidget id="btnThemeMob" caption="&#x263D;" description="Alternar tema" visible="false" width="100%">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('doToggleTheme');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<DateField id="dtIni" type="date" caption="De" width="100%" resolution="DAY" />
<DateField id="dtFim" type="date" caption="Até" width="100%" resolution="DAY" />
<NumericField id="nmfSla" type="number" caption="SLA (dias)" width="100%" />
<ComboBox id="cmbExecucao" caption="Execução" width="100%" allowNullSelection="true">
<entry key="1" value="Interno" />
<entry key="2" value="Externo" />
</ComboBox>
<ButtonWidget id="btnFiltrar" caption="Pesquisar" style="BLUE" width="100%">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<Label width="100%" expandRatio="1" />
</VerticalLayout>
<Panel width="100%" id="contentPanel" height="100%" margin="false" expandRatio="1">
<VerticalLayout align="TOP_LEFT" spacing="false" margin="false" id="panelRoot" height="100%">
<ScriptWidget id="scriptDash" height="100%" expandRatio="1">
<factory>
<LayoutFactory type="VerticalLayout" width="100%" height="100%" />
</factory>
<InitScript language="JavaScript">
<![CDATA[
var components = libService.loadScript('vaadinComponents');
var db = libService.loadScript('db');
var banco = new db(db.VITRUVIO_DATASOURCE);
var base = null;
function getTheme() { return String(engine.getGlobalVariable('theme') || 'dark'); }
function isLight() { return getTheme() === 'light'; }
function isMobileCtx() {
return engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender');
}
function svgDonut(dentro, fora) {
var total = dentro + fora;
if (total === 0) return '<div style="color:#8899aa;font-size:11px;text-align:center;padding:10px;">Sem dados</div>';
var cx=65,cy=65,ro=56,ri=34,pD=dentro/total,pF=fora/total;
function arc(sa,ea) {
var lg=(ea-sa>Math.PI)?1:0;
return 'M '+(cx+ro*Math.cos(sa))+' '+(cy+ro*Math.sin(sa))+
' A '+ro+' '+ro+' 0 '+lg+' 1 '+(cx+ro*Math.cos(ea))+' '+(cy+ro*Math.sin(ea))+
' L '+(cx+ri*Math.cos(ea))+' '+(cy+ri*Math.sin(ea))+
' A '+ri+' '+ri+' 0 '+lg+' 0 '+(cx+ri*Math.cos(sa))+' '+(cy+ri*Math.sin(sa))+' Z';
}
var svg='<svg viewBox="0 0 130 130" width="88" height="88">';
if (pD>=0.9999) {
svg+='<circle cx="65" cy="65" r="56" fill="#4a9edd"/><circle class="donut-hole" cx="65" cy="65" r="34"/>';
svg+='<text x="65" y="61" text-anchor="middle" font-family="Arial" font-size="11" font-weight="bold" fill="#4a9edd">100%</text>';
svg+='<text x="65" y="74" text-anchor="middle" font-family="Arial" font-size="9" fill="#8899aa">Dentro</text>';
} else if (pF>=0.9999) {
svg+='<circle cx="65" cy="65" r="56" fill="#e74c3c"/><circle class="donut-hole" cx="65" cy="65" r="34"/>';
svg+='<text x="65" y="61" text-anchor="middle" font-family="Arial" font-size="11" font-weight="bold" fill="#e74c3c">100%</text>';
svg+='<text x="65" y="74" text-anchor="middle" font-family="Arial" font-size="9" fill="#8899aa">Fora</text>';
} else {
var sa1=-Math.PI/2, ea1=sa1+pD*2*Math.PI;
svg+='<path d="'+arc(sa1,ea1)+'" fill="#4a9edd"/>';
svg+='<path d="'+arc(ea1,ea1+pF*2*Math.PI)+'" fill="#e74c3c"/>';
svg+='<circle class="donut-hole" cx="65" cy="65" r="34"/>';
if(pD>0.08){var mD=sa1+pD*Math.PI;svg+='<text x="'+(cx+(ro+ri)/2*Math.cos(mD))+'" y="'+(cy+(ro+ri)/2*Math.sin(mD)+4)+'" text-anchor="middle" font-family="Arial" font-size="10" font-weight="bold" fill="#fff">'+Math.round(pD*100)+'%</text>';}
if(pF>0.08){var mF=ea1+pF*Math.PI;svg+='<text x="'+(cx+(ro+ri)/2*Math.cos(mF))+'" y="'+(cy+(ro+ri)/2*Math.sin(mF)+4)+'" text-anchor="middle" font-family="Arial" font-size="10" font-weight="bold" fill="#fff">'+Math.round(pF*100)+'%</text>';}
}
return svg+'</svg>';
}
function svgHBar(dados, barColor) {
if (!dados || dados.length===0) return '<div style="color:#8899aa;font-size:11px;padding:8px;text-align:center;">Sem dados</div>';
var light=isLight(), lblC=light?'#64748b':'#8899aa', valC=light?'#475569':'#aabbcc';
var maxVal=0;
for (var i=0;i<dados.length;i++) if(dados[i].value>maxVal) maxVal=dados[i].value;
if(maxVal===0) maxVal=1;
var bg=light?'#e2e8f0':'#243447';
var lblW = isMobileCtx() ? '110px' : '140px';
var out='<div style="width:100%;">';
for (var i=0;i<dados.length;i++) {
var barPct=Math.round((dados[i].value/maxVal)*100);
var lbl=String(dados[i].label); if(lbl.length>20) lbl=lbl.substring(0,18)+'.';
out+='<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;">';
out+='<div style="width:'+lblW+';text-align:right;font-size:12px;color:'+lblC+';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0;">'+lbl+'</div>';
out+='<div style="flex:1;background:'+bg+';border-radius:2px;height:10px;overflow:hidden;">';
out+='<div style="width:'+barPct+'%;height:10px;background:'+barColor+';border-radius:2px;"></div>';
out+='</div>';
out+='<div style="font-size:12px;color:'+valC+';white-space:nowrap;width:30px;flex-shrink:0;">'+dados[i].value+'</div>';
out+='</div>';
}
return out+'</div>';
}
function renderHtml(html) {
var comp = components.html(html);
comp.setSizeFull();
base.removeAllComponents();
base.addComponent(comp);
base.setExpandRatio(comp, 1.0);
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"setTimeout(function(){" +
"if(typeof dtechApplyTheme==='function')dtechApplyTheme();" +
// IMPORTANTE: NÃO redimensionar .mobile-root-frame para caber o conteúdo
// aqui. A altura do frame é definida UMA ÚNICA VEZ no bloco _mobileRender
// do run() (via window.screen.height) e deve permanecer FIXA — é essa
// altura fixa que faz a topBar (irmã de mainArea dentro de rootLayout,
// fora de .dash-wrapper) nunca fazer parte de nenhum scroll: ela só
// ocupa sua altura natural, e mainArea (expandRatio=1) preenche o
// restante, com .dash-wrapper (overflow-y:auto) rolando por dentro desse
// espaço restante. A versão anterior recalculava a altura do frame para
// "el.getBoundingClientRect().bottom" a cada render — isso fazia o frame
// crescer para caber TODO o conteúdo (não só o viewport), transformando
// topBar+conteúdo num único bloco comprido que a página externa rolava
// inteiro, "arrastando" a topBar junto.
"var el=document.getElementById('dtech-main');" +
"if(el){" +
"var p=el,n=0;" +
"while(p&&n<30){p=p.parentElement;n++;" +
"if(!p){break;}" +
"if(p.classList&&p.classList.contains('mobile-root-frame')){break;}" +
"var cs=window.getComputedStyle(p);" +
"if(cs&&(cs.overflowY==='auto'||cs.overflowY==='scroll'||cs.overflowY==='hidden')){p.style.overflowY='visible';}" +
"if(cs&&(cs.overflow==='auto'||cs.overflow==='scroll'||cs.overflow==='hidden')){p.style.overflow='visible';}" +
"}" +
"}" +
"},400);"
);
}
function runQuery(dtIniVal, dtFimVal, execVal) {
var params={};
var sql =
"WITH etapa_atual AS ( " +
" SELECT DISTINCT ON (instancia_fk) instancia_fk AS processo, form_key " +
" FROM formulario WHERE form_key IN ('formRealizarLaudo','formExecutarOS') " +
" ORDER BY instancia_fk, ultima_alteracao DESC " +
"), task_etapa AS ( " +
" SELECT DISTINCT ON (ahi.proc_inst_id_) " +
" ahi.proc_inst_id_, ahi.start_time_ AS data_abertura_etapa, " +
" ahi.end_time_ AS data_fechamento_etapa, ahi.assignee_ AS tecnico_login " +
" FROM act_hi_taskinst ahi " +
" WHERE ahi.form_key_ IN ('formRealizarLaudo','formExecutarOS') " +
" ORDER BY ahi.proc_inst_id_, ahi.start_time_ DESC " +
"), data_distribuicao AS ( " +
" SELECT DISTINCT ON (ahi.proc_inst_id_) " +
" ahi.proc_inst_id_, ahi.end_time_ AS data_distribuicao " +
" FROM act_hi_taskinst ahi " +
" WHERE ahi.form_key_ = 'formDistribuirChamado' " +
" ORDER BY ahi.proc_inst_id_, ahi.start_time_ DESC " +
") " +
"SELECT vao.processo, pi.bpmn_process_instance, " +
" CASE vao.tipo_atendimento " +
" WHEN 1 THEN 'Avulso' WHEN 2 THEN 'Contrato' WHEN 3 THEN 'Garantia' " +
" WHEN 4 THEN 'Atendimento Interno' ELSE 'Pendente' END AS tipo, " +
" CASE vao.interno_externo " +
" WHEN '1' THEN 'Interno' WHEN '2' THEN 'Externo' ELSE 'N/I' END AS tipo_execucao, " +
" COALESCE(NULLIF(vp_cli.nome,''), vp_cli.nome_fantasia) AS cliente, " +
" vp_prod.nome AS equipamento, " +
" COALESCE(vam.nome, SUBSTRING(vp_prod.nome FROM 1 FOR 10)) AS setor, " +
" COALESCE(vao.numero_serie,'') AS numero_serie, " +
" COALESCE(vao.solicitante,'') AS solicitante, " +
" TO_CHAR(vao.previsao_atendimento,'DD/MM/YYYY') AS previsao_atendimento_fmt, " +
" TO_CHAR(pi.data_abertura,'DD/MM/YYYY') AS data_abertura, " +
" TO_CHAR(pi.data_abertura,'YYYYMMDD') AS data_abertura_sort, " +
" TO_CHAR(pi.data_encerramento,'DD/MM/YYYY') AS data_encerramento, " +
" TO_CHAR(te.data_abertura_etapa,'DD/MM/YYYY') AS data_abertura_etapa_fmt, " +
" TO_CHAR(te.data_abertura_etapa,'YYYYMMDD') AS data_abertura_etapa_sort, " +
" TO_CHAR(te.data_fechamento_etapa,'DD/MM/YYYY') AS data_fechamento_fmt, " +
" TO_CHAR(te.data_fechamento_etapa,'YYYYMMDD') AS data_fechamento_sort, " +
" CASE ea.form_key WHEN 'formRealizarLaudo' THEN 'Realizar Laudo' WHEN 'formExecutarOS' THEN 'Executar O.S' END AS etapa, " +
" CASE pi.status WHEN 1 THEN 'Aberto' WHEN 3 THEN 'Cancelado' WHEN 4 THEN 'Finalizado' ELSE CAST(pi.status AS VARCHAR) END AS status_processo, " +
" te.data_abertura_etapa, te.data_fechamento_etapa, " +
" (te.data_fechamento_etapa::date - te.data_abertura_etapa::date) AS dias_sla_pg, " +
" COALESCE(vl.nome, te.tecnico_login) AS tecnico_nome " +
"FROM vrp_atendimento_os vao " +
"INNER JOIN processo_instancia pi ON pi.instancia_id = vao.processo " +
"INNER JOIN vrp_pessoas vp_cli ON vp_cli.id = vao.cliente " +
"INNER JOIN vrp_produto vp_prod ON vp_prod.id = vao.produto_fk " +
"LEFT JOIN vrp_arvore_mercadologica vam ON vam.id = vp_prod.nivel1 AND vam.nivel = 1 " +
"INNER JOIN etapa_atual ea ON ea.processo = vao.processo " +
"LEFT JOIN task_etapa te ON te.proc_inst_id_ = pi.bpmn_process_instance " +
"LEFT JOIN data_distribuicao dd ON dd.proc_inst_id_ = pi.bpmn_process_instance " +
"LEFT JOIN nauth.usuario vl ON vl.login = te.tecnico_login " +
"WHERE te.data_fechamento_etapa IS NOT NULL ";
if (dtIniVal!=null) { sql+=" AND te.data_fechamento_etapa >= :dtIni "; params['dtIni']=dtIniVal; }
if (dtFimVal!=null) { sql+=" AND te.data_fechamento_etapa <= :dtFim "; params['dtFim']=dtFimVal; }
if (execVal!=null && String(execVal)!=='') sql+=" AND vao.interno_externo = '"+String(execVal)+"' ";
sql+=" ORDER BY te.data_fechamento_etapa DESC ";
var linhas=[];
var rows=banco.query(sql,params);
if(rows){
rows.each(function(row){
var diasSla=null;
if(row.DIAS_SLA_PG!=null)
diasSla=Number(row.DIAS_SLA_PG);
linhas.push({
processo:String(row.PROCESSO||''),
bpmnPi:String(row.BPMN_PROCESS_INSTANCE||''),
tipo:String(row.TIPO||''),
tipoExecucao:String(row.TIPO_EXECUCAO||''),
cliente:String(row.CLIENTE||''),
equipamento:String(row.EQUIPAMENTO||''),
setor:String(row.SETOR||''),
numeroSerie:String(row.NUMERO_SERIE||''),
solicitante:String(row.SOLICITANTE||''),
previsao:String(row.PREVISAO_ATENDIMENTO_FMT||''),
dataAbertura:String(row.DATA_ABERTURA||''),
dataAberturaSort:String(row.DATA_ABERTURA_SORT||''),
dataEncerramento:String(row.DATA_ENCERRAMENTO||''),
dataAberturaEtapaFmt:String(row.DATA_ABERTURA_ETAPA_FMT||''),
dataAberturaEtapaSort:String(row.DATA_ABERTURA_ETAPA_SORT||''),
dataFechamentoFmt:String(row.DATA_FECHAMENTO_FMT||''),
dataFechamentoSort:String(row.DATA_FECHAMENTO_SORT||''),
etapa:String(row.ETAPA||''),
status:String(row.STATUS_PROCESSO||''),
tecNome:String(row.TECNICO_NOME!=null?row.TECNICO_NOME:'N/I'),
diasSla:diasSla
});
});
}
return linhas;
}
/* Contexto IA (Pietro) — "secoes" usa os mesmos títulos exibidos na tela.
dados_contexto é sempre SUBSTITUÍDO por inteiro (nunca mesclado). */
function atualizarContextoIA(view, filtros, secoes) {
var st = engine.getGlobalVariable('chatAgenteState');
if (!st || !st.config) return;
st.config.dados_contexto = { view: view, filtros: filtros, secoes: secoes };
}
/* ---------------- DASHBOARD ---------------- */
function renderDash() {
var slaLimite=Number(engine.getField('nmfSla').getValue())||3;
var linhas=runQuery(engine.getField('dtIni').getValue(),engine.getField('dtFim').getValue(),engine.getField('cmbExecucao').getValue());
var tecMap={},tecCliMap={},tecSetorMap={},tipoMap={},cliMap={},setorMap={},slaDentro=0,slaFora=0;
for(var i=0;i<linhas.length;i++){
var l=linhas[i];
if(!tecMap[l.tecNome]){tecMap[l.tecNome]={total:0,dentro:0,fora:0};tecCliMap[l.tecNome]={};tecSetorMap[l.tecNome]={};}
tecMap[l.tecNome].total++;
if(!tecCliMap[l.tecNome][l.cliente])tecCliMap[l.tecNome][l.cliente]=0;
tecCliMap[l.tecNome][l.cliente]++;
if(!tipoMap[l.tipo])tipoMap[l.tipo]=0;tipoMap[l.tipo]++;
if(!cliMap[l.cliente])cliMap[l.cliente]=0;cliMap[l.cliente]++;
var sKey=l.setor&&l.setor!==''?l.setor:'N/I';
if(!setorMap[sKey])setorMap[sKey]=0;setorMap[sKey]++;
if(!tecSetorMap[l.tecNome][sKey])tecSetorMap[l.tecNome][sKey]=0;
tecSetorMap[l.tecNome][sKey]++;
if(l.diasSla!==null){if(l.diasSla<=slaLimite){slaDentro++;tecMap[l.tecNome].dentro++;}else{slaFora++;tecMap[l.tecNome].fora++;}}
}
var tipoArr=[];for(var k in tipoMap)tipoArr.push({label:k,value:tipoMap[k]});
tipoArr.sort(function(a,b){return b.value-a.value;});
var cliArr=[];for(var k in cliMap)cliArr.push({label:k,value:cliMap[k]});
cliArr.sort(function(a,b){return b.value-a.value;});
if(cliArr.length>15)cliArr=cliArr.slice(0,15);
var setorArr=[];for(var k in setorMap)setorArr.push({label:k,value:setorMap[k]});
setorArr.sort(function(a,b){return b.value-a.value;});
var tecArr=[];
for(var k in tecMap){
var t=tecMap[k],com=t.dentro+t.fora;
var topCli='',topCnt=0,numCli=0;
var cmap=tecCliMap[k]||{};
for(var ck in cmap){numCli++;if(cmap[ck]>topCnt){topCnt=cmap[ck];topCli=ck;}}
var topEquip='',topECnt=0,numEquip=0;
var emap=tecSetorMap[k]||{};
for(var ek in emap){numEquip++;if(emap[ek]>topECnt){topECnt=emap[ek];topEquip=ek;}}
tecArr.push({nome:k,total:t.total,dentro:t.dentro,fora:t.fora,comSla:com,
pDentro:com>0?Math.round(t.dentro/com*100):-1,
pFora:com>0?Math.round(t.fora/com*100):-1,
topCliente:topCli,numClientes:numCli,topEquip:topEquip,numEquips:numEquip});
}
tecArr.sort(function(a,b){return b.total-a.total;});
var maxTotal=tecArr.length>0?tecArr[0].total:1;
var slaTotal=slaDentro+slaFora;
var themeClass=isLight()?' theme-light':'';
var mobileClass=isMobileCtx()?' mobile-view':'';
var html='<div class="dash-wrapper'+themeClass+mobileClass+'" id="dtech-main">';
html+='<div class="dtech-legend">&#9432; <b>Realizar Laudo</b> e <b>Executar O.S</b> &mdash; per&iacute;odo pela data de fechamento da etapa.</div>';
/* ── KPIs ── */
var kpiFlex = mobileClass ? 'flex-direction:column;' : '';
html+='<div style="display:flex;gap:8px;margin-bottom:8px;align-items:stretch;flex-wrap:wrap;'+kpiFlex+'">';
html+='<div class="dash-kpi"><div class="dash-kpi-label">Total</div><div class="dash-kpi-value">'+linhas.length+'</div></div>';
html+='<div class="dash-kpi" style="background:linear-gradient(135deg,#1a3d2b,#1a2733);"><div class="dash-kpi-label">Dentro (&le;'+slaLimite+'d)</div><div class="dash-kpi-value" style="color:#2ecc71;">'+slaDentro+(slaTotal>0?' <span style="font-size:14px;font-weight:normal;color:#8899aa;">('+Math.round(slaDentro/slaTotal*100)+'%)</span>':'')+'</div></div>';
html+='<div class="dash-kpi" style="background:linear-gradient(135deg,#3d1a1a,#1a2733);"><div class="dash-kpi-label">Fora do Prazo</div><div class="dash-kpi-value" style="color:#e74c3c;">'+slaFora+(slaTotal>0?' <span style="font-size:14px;font-weight:normal;color:#8899aa;">('+Math.round(slaFora/slaTotal*100)+'%)</span>':'')+'</div></div>';
if (!mobileClass) {
/* Desktop: SLA donut e Tipo de Serviço na mesma linha dos KPIs */
html+='<div class="dash-chart-box" style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px 10px;min-width:140px;margin-bottom:0;">';
html+='<div class="dash-chart-title" style="margin-bottom:3px;">SLA</div>';
html+=svgDonut(slaDentro,slaFora);
html+='<div style="font-size:10px;color:#8899aa;margin-top:2px;"><span style="color:#4a9edd;">&#9632;</span> Dentro &nbsp;<span style="color:#e74c3c;">&#9632;</span> Fora</div>';
html+='</div>';
html+='<div class="dash-chart-box" style="flex:1;min-width:180px;margin-bottom:0;">';
html+='<div class="dash-chart-title">Tipo de Servi&ccedil;o</div>';
html+=svgHBar(tipoArr,'#4a9edd');
html+='</div>';
}
html+='</div>';
if (mobileClass) {
/* Mobile: SLA donut e Tipo de Serviço empilhados */
html+='<div class="dash-chart-box" style="display:flex;flex-direction:column;align-items:center;padding:10px;margin-bottom:8px;">';
html+='<div class="dash-chart-title" style="margin-bottom:6px;font-size:13px;">SLA &mdash; '+slaTotal+' com registro</div>';
html+=svgDonut(slaDentro,slaFora);
html+='<div style="font-size:13px;color:#8899aa;margin-top:6px;"><span style="color:#4a9edd;">&#9632;</span> Dentro &nbsp;<span style="color:#e74c3c;">&#9632;</span> Fora</div>';
html+='</div>';
html+='<div class="dash-chart-box" style="margin-bottom:8px;">';
html+='<div class="dash-chart-title" style="font-size:13px;">Tipo de Servi&ccedil;o</div>';
html+=svgHBar(tipoArr,'#4a9edd');
html+='</div>';
}
/* ── Técnicos ── */
if (!mobileClass) {
/* Desktop: tabela */
html+='<div class="dash-chart-box">';
html+='<div class="dash-chart-title"><span>T&eacute;cnicos &mdash; Chamados e SLA</span><span style="font-size:10px;font-weight:normal;letter-spacing:0;">clique na coluna para ordenar</span></div>';
html+='<div style="max-height:230px;overflow-y:auto;">';
html+='<table id="tec-tbl" class="dtech-tec-table" data-sc="total" data-sd="-1"><thead><tr>';
html+='<th style="width:24px;text-align:center;">#</th>';
html+='<th data-l="T&eacute;cnico">T&eacute;cnico</th>';
html+='<th data-l="Principal Cliente">Principal Cliente</th>';
html+='<th data-l="Principal Equipamento">Principal Equipamento</th>';
html+='<th data-l="Total" data-col="total" class="sortable" onclick="dtechTecSort(\'total\')" style="text-align:center;">Total &#9660;</th>';
html+='<th style="min-width:86px;">Volume</th>';
html+='<th data-l="% Ok" data-col="pdentro" class="sortable" onclick="dtechTecSort(\'pdentro\')" style="text-align:center;">% Ok</th>';
html+='<th style="min-width:116px;">SLA</th>';
html+='</tr></thead><tbody>';
for(var i=0;i<tecArr.length;i++){
var t=tecArr[i],volPct=Math.round(t.total/maxTotal*100);
var slaBD=t.comSla>0?Math.round(t.dentro/t.comSla*100):0;
var slaBF=t.comSla>0?Math.round(t.fora/t.comSla*100):0;
var pDStr=t.pDentro>=0?(t.pDentro+'%'):'&mdash;';
var cliDisp=t.topCliente||'&mdash;';
if(t.numClientes>1)cliDisp+=' <span style="color:#556677;font-size:10px;">(+' + (t.numClientes-1) + ')</span>';
var equipDisp=t.topEquip||'&mdash;';
if(t.numEquips>1)equipDisp+=' <span style="color:#556677;font-size:10px;">(+' + (t.numEquips-1) + ')</span>';
var volBar='<div style="display:flex;align-items:center;"><div style="width:78px;height:8px;background:#243447;border-radius:2px;"><div style="width:'+volPct+'%;height:8px;background:#4a9edd;border-radius:2px;"></div></div></div>';
var slaBar='<div style="display:flex;align-items:center;gap:4px;"><div style="display:flex;width:96px;height:9px;border-radius:2px;overflow:hidden;background:#243447;"><div style="width:'+slaBD+'%;background:#2ecc71;"></div><div style="width:'+slaBF+'%;background:#e74c3c;"></div></div><span style="font-size:10px;color:#8899aa;">'+pDStr+'</span></div>';
html+='<tr data-total="'+t.total+'" data-dentro="'+t.dentro+'" data-pfora="'+(t.pFora>=0?t.pFora:0)+'" data-pdentro="'+(t.pDentro>=0?t.pDentro:0)+'">';
html+='<td style="text-align:center;color:#556677;">'+(i+1)+'</td>';
html+='<td><b>'+t.nome+'</b></td>';
html+='<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;">'+cliDisp+'</td>';
html+='<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;">'+equipDisp+'</td>';
html+='<td style="text-align:center;font-weight:bold;color:#4a9edd;">'+t.total+'</td>';
html+='<td>'+volBar+'</td>';
html+='<td style="text-align:center;">'+pDStr+'</td>';
html+='<td>'+slaBar+'</td>';
html+='</tr>';
}
if(tecArr.length===0)html+='<tr><td colspan="8" style="text-align:center;padding:20px;color:#556677;">Sem dados</td></tr>';
html+='</tbody></table></div></div>';
} else {
/* Mobile: cards compactos de técnicos (3 linhas) + ordenação */
html+='<div class="dash-chart-box" style="margin-bottom:4px;">';
html+='<div style="display:flex;align-items:center;justify-content:space-between;">';
html+='<span style="color:#8899aa;font-size:13px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">T&eacute;cnicos</span>';
html+='<div style="display:flex;gap:5px;">';
html+='<button id="tec-sort-total" class="tec-sort-btn active" onclick="tecSortCards(\'total\')">Total &#9660;</button>';
html+='<button id="tec-sort-sla" class="tec-sort-btn" onclick="tecSortCards(\'pdentro\')">% Ok</button>';
html+='</div>';
html+='</div>';
html+='</div>';
html+='<div class="tec-mob-scroll">';
html+='<div id="tec-mob-list" data-sc="total" data-sd="-1">';
for(var i=0;i<tecArr.length;i++){
var t=tecArr[i];
var pDStr=t.pDentro>=0?(t.pDentro+'%'):'—';
var slaBD=t.comSla>0?Math.round(t.dentro/t.comSla*100):0;
var slaBF=t.comSla>0?Math.round(t.fora/t.comSla*100):0;
var cliDisp=t.topCliente||'';
if(t.numClientes>1)cliDisp+=' (+' + (t.numClientes-1) + ')';
var equipDisp=t.topEquip||'';
if(t.numEquips>1)equipDisp+=' (+' + (t.numEquips-1) + ')';
var slaColor=t.pDentro<0?'#556677':(t.pDentro>=70?'#2ecc71':'#e67e22');
var slaInfo=t.comSla>0
?('<span style="color:#2ecc71;">'+t.dentro+'&#x2193;</span>&nbsp;<span style="color:#e74c3c;">'+t.fora+'&#x2191;</span> &middot; <b style="color:'+slaColor+';">'+pDStr+'</b>')
:'<span style="color:#556677;">sem SLA</span>';
html+='<div class="tec-mob-card" data-total="'+t.total+'" data-pdentro="'+(t.pDentro>=0?t.pDentro:0)+'">';
html+='<div class="tec-mob-row1">';
html+='<span class="tec-mob-rank">#'+(i+1)+'</span>';
html+='<span class="tec-mob-nome">'+t.nome+'</span>';
html+='<span class="tec-mob-total">'+t.total+'</span>';
html+='</div>';
html+='<div class="tec-mob-sla">';
html+='<div class="tec-mob-sla-bar"><div style="display:flex;height:100%;"><div style="width:'+slaBD+'%;background:#2ecc71;"></div><div style="width:'+slaBF+'%;background:#e74c3c;"></div></div></div>';
html+='<span class="tec-mob-sla-info">'+slaInfo+'</span>';
html+='</div>';
var metaLine='';
if(cliDisp)metaLine+=cliDisp;
if(equipDisp)metaLine+=(metaLine?' &middot; ':'')+equipDisp;
if(metaLine)html+='<div class="tec-mob-cli">'+metaLine+'</div>';
html+='</div>';
}
if(tecArr.length===0)html+='<div style="padding:20px;text-align:center;color:#556677;">Sem dados</div>';
html+='</div>';
html+='</div>';
}
/* ── Cliente + Equipamentos ── */
if (!mobileClass) {
html+='<div style="display:flex;gap:8px;">';
html+='<div class="dash-chart-box" style="flex:1;min-width:0;">';
html+='<div class="dash-chart-title">Cliente (Top 15)</div>';
html+='<div style="max-height:220px;overflow-y:auto;">'+svgHBar(cliArr,'#4a9edd')+'</div>';
html+='</div>';
html+='<div class="dash-chart-box" style="flex:1;min-width:0;">';
html+='<div class="dash-chart-title">Equipamentos</div>';
html+='<div style="max-height:220px;overflow-y:auto;">'+svgHBar(setorArr,'#e67e22')+'</div>';
html+='</div>';
html+='</div>';
} else {
/* Mobile: empilhados */
html+='<div class="dash-chart-box" style="margin-bottom:8px;">';
html+='<div class="dash-chart-title" style="font-size:13px;">Cliente (Top 15)</div>';
html+='<div class="hbar-scroll">'+svgHBar(cliArr,'#4a9edd')+'</div>';
html+='</div>';
html+='<div class="dash-chart-box">';
html+='<div class="dash-chart-title" style="font-size:13px;">Equipamentos</div>';
html+='<div class="hbar-scroll">'+svgHBar(setorArr,'#e67e22')+'</div>';
html+='</div>';
}
html+='</div>';
var _tecCtxIA=[];
for(var _ti=0;_ti<tecArr.length;_ti++){
var _t=tecArr[_ti];
_tecCtxIA.push({tecnico:_t.nome,total:_t.total,dentro_sla:_t.dentro,fora_sla:_t.fora,percentual_dentro:_t.pDentro,principal_cliente:_t.topCliente,principal_equipamento:_t.topEquip});
}
atualizarContextoIA('dashboard', {dt_ini:String(engine.getField('dtIni').getValue()),dt_fim:String(engine.getField('dtFim').getValue()),sla_limite:slaLimite,execucao:String(engine.getField('cmbExecucao').getValue()||'')}, [
{titulo:'Total',tipo:'kpi',dados:{valor:linhas.length}},
{titulo:'Dentro (\u2264'+slaLimite+'d)',tipo:'kpi',dados:{valor:slaDentro,percentual:slaTotal>0?Math.round(slaDentro/slaTotal*100):null}},
{titulo:'Fora do Prazo',tipo:'kpi',dados:{valor:slaFora,percentual:slaTotal>0?Math.round(slaFora/slaTotal*100):null}},
{titulo:'SLA',tipo:'grafico_donut',dados:{dentro:slaDentro,fora:slaFora}},
{titulo:'Tipo de Serviço',tipo:'grafico_barras',dados:tipoArr},
{titulo:'Técnicos — Chamados e SLA',tipo:'tabela',dados:_tecCtxIA},
{titulo:'Cliente (Top 15)',tipo:'grafico_barras',dados:cliArr},
{titulo:'Equipamentos',tipo:'grafico_barras',dados:setorArr}
]);
renderHtml(html);
}
/* ---------------- MOVIMENTAÇÃO ---------------- */
function renderMov() {
var slaLimite=Number(engine.getField('nmfSla').getValue())||3;
var linhas=runQuery(engine.getField('dtIni').getValue(),engine.getField('dtFim').getValue(),engine.getField('cmbExecucao').getValue());
var slaDentro=0,slaFora=0;
for(var i=0;i<linhas.length;i++){
if(linhas[i].diasSla!==null){if(linhas[i].diasSla<=slaLimite)slaDentro++;else slaFora++;}
}
var slaTotal=slaDentro+slaFora;
var themeClass=isLight()?' theme-light':'';
var mobileClass=isMobileCtx()?' mobile-view':'';
var html='<div class="dash-wrapper'+themeClass+mobileClass+'" id="dtech-main">';
/* KPIs */
var kpiFlex = mobileClass ? 'flex-direction:column;' : '';
html+='<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap;'+kpiFlex+'">';
html+='<div class="dash-kpi" style="padding:8px 14px;"><div class="dash-kpi-label">Total OS</div><div class="dash-kpi-value">'+linhas.length+'</div></div>';
html+='<div class="dash-kpi" style="padding:8px 14px;background:linear-gradient(135deg,#1a3d2b,#1a2733);"><div class="dash-kpi-label">Dentro</div><div class="dash-kpi-value" style="color:#2ecc71;">'+slaDentro+(slaTotal>0?' <span style="font-size:14px;font-weight:normal;color:#8899aa;">('+Math.round(slaDentro/slaTotal*100)+'%)</span>':'')+'</div></div>';
html+='<div class="dash-kpi" style="padding:8px 14px;background:linear-gradient(135deg,#3d1a1a,#1a2733);"><div class="dash-kpi-label">Fora</div><div class="dash-kpi-value" style="color:#e74c3c;">'+slaFora+(slaTotal>0?' <span style="font-size:14px;font-weight:normal;color:#8899aa;">('+Math.round(slaFora/slaTotal*100)+'%)</span>':'')+'</div></div>';
html+='</div>';
if (!mobileClass) {
/* Desktop: toolbar + tabela */
var selStyle='background:#1a2733;border:1px solid #243447;color:#ccc;font-size:10px;padding:3px 6px;border-radius:4px;cursor:pointer;';
html+='<div class="mov-toolbar">';
html+='<input type="text" id="mov-fi" class="mov-filter-input" placeholder="Filtrar por N&ordm;, t&eacute;cnico, cliente, equipamento..." oninput="movFilter()">';
html+='<select id="mov-stf" onchange="movFilter()" style="'+selStyle+'">';
html+='<option value="">Tipo: Todos</option>';
html+='<option value="avulso">Avulso</option>';
html+='<option value="contrato">Contrato</option>';
html+='<option value="garantia">Garantia</option>';
html+='<option value="atendimento interno">Atendimento Interno</option>';
html+='<option value="pendente">Pendente</option>';
html+='</select>';
html+='<select id="mov-sef" onchange="movFilter()" style="'+selStyle+'">';
html+='<option value="">Execu&ccedil;&atilde;o: Todos</option>';
html+='<option value="interno">Interno</option>';
html+='<option value="externo">Externo</option>';
html+='<option value="n/i">N/I</option>';
html+='</select>';
html+='<span id="mov-rc" style="color:#8899aa;font-size:10px;white-space:nowrap;">'+linhas.length+' registro(s)</span>';
html+='<button onclick="movExportCSV()" class="mov-export-btn">&#8615; CSV</button>';
html+='</div>';
html+='<div style="overflow-x:scroll;width:100%;padding-bottom:2px;">';
var thS='style="cursor:pointer;user-select:none;"';
html+='<table id="mov-tbl" class="mov-table" style="min-width:1100px;"><thead><tr>';
html+='<th data-l="N&ordm; Chamado" onclick="movSort(0,0)" '+thS+'>N&ordm; Chamado</th>';
html+='<th data-l="T&eacute;cnico" onclick="movSort(1,0)" '+thS+'>T&eacute;cnico</th>';
html+='<th data-l="Cliente" onclick="movSort(2,0)" '+thS+'>Cliente</th>';
html+='<th data-l="Tipo" onclick="movSort(3,0)" '+thS+'>Tipo</th>';
html+='<th data-l="Execu&ccedil;&atilde;o" onclick="movSort(4,0)" '+thS+'>Execu&ccedil;&atilde;o</th>';
html+='<th data-l="Data Abertura" onclick="movSort(5,1)" '+thS+'>Data Abertura</th>';
html+='<th data-l="Data Ab. Etapa" onclick="movSort(6,1)" '+thS+'>Data Ab. Etapa</th>';
html+='<th data-l="Data Fech. Etapa" onclick="movSort(7,1)" '+thS+'>Data Fech. Etapa</th>';
html+='<th data-l="Status" onclick="movSort(8,0)" '+thS+'>Status</th>';
html+='<th data-l="SLA (d)" onclick="movSort(9,1)" style="cursor:pointer;user-select:none;text-align:center;">SLA (d)</th>';
html+='<th>Prazo</th>';
html+='</tr></thead><tbody>';
for(var i=0;i<linhas.length;i++){
var l=linhas[i],slaNum=l.diasSla!==null?l.diasSla:-1;
var slaStr=l.diasSla!==null?String(l.diasSla):'&mdash;';
var ph=l.diasSla===null?'<span style="color:#556677;">—</span>'
:(l.diasSla<=slaLimite?'<span class="dtech-badge-ok">&#10003; Dentro</span>'
:'<span class="dtech-badge-fora">&#10007; Fora</span>');
var detId='mdet-'+i;
html+='<tr class="dtech-mov-row"'+
' data-equip="'+l.equipamento.replace(/"/g,'&quot;')+'"'+
' data-serie="'+l.numeroSerie.replace(/"/g,'&quot;')+'"'+
' data-solic="'+l.solicitante.replace(/"/g,'&quot;')+'"'+
' data-prev="'+l.previsao.replace(/"/g,'&quot;')+'"'+
' data-tipo="'+l.tipo.replace(/"/g,'&quot;')+'"'+
' data-exec="'+l.tipoExecucao.replace(/"/g,'&quot;')+'"'+
' data-etapa="'+l.etapa.replace(/"/g,'&quot;')+'"'+
' onclick="var d=document.getElementById(\''+detId+'\');d.style.display=(d.style.display===\'\'||d.style.display===\'none\'?\'table-row\':\'none\');">';
var procLink=l.bpmnPi
?'<a href="#" onclick="event.stopPropagation();parent.vitruvio.showProcessData(\''+l.bpmnPi+'\');return false;" style="color:#4a9edd;text-decoration:none;">'+l.processo+'</a>'
:l.processo;
html+='<td data-v="'+l.processo+'">'+procLink+'</td>';
html+='<td data-v="'+l.tecNome.replace(/"/g,'&quot;')+'">'+l.tecNome+'</td>';
html+='<td data-v="'+l.cliente.replace(/"/g,'&quot;')+'">'+l.cliente+'</td>';
html+='<td data-v="'+l.tipo+'">'+l.tipo+'</td>';
html+='<td data-v="'+l.tipoExecucao+'">'+l.tipoExecucao+'</td>';
html+='<td data-v="'+l.dataAberturaSort+'">'+l.dataAbertura+'</td>';
html+='<td data-v="'+l.dataAberturaEtapaSort+'">'+l.dataAberturaEtapaFmt+'</td>';
html+='<td data-v="'+l.dataFechamentoSort+'">'+l.dataFechamentoFmt+'</td>';
html+='<td data-v="'+l.status+'">'+l.status+'</td>';
html+='<td data-v="'+slaNum+'" style="text-align:center;">'+slaStr+'</td>';
html+='<td>'+ph+'</td></tr>';
html+='<tr id="'+detId+'" class="dtech-mov-detail" style="display:none;">';
html+='<td colspan="11" style="padding:8px 12px;">';
html+='<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:11px;">';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Equipamento</div><span style="color:#ccc;">'+l.equipamento+'</span></div>';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">N&ordm; S&eacute;rie</div><span style="color:#ccc;">'+(l.numeroSerie||'&mdash;')+'</span></div>';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Solicitante</div><span style="color:#ccc;">'+(l.solicitante||'&mdash;')+'</span></div>';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Previs&atilde;o Atend.</div><span style="color:#ccc;">'+(l.previsao||'&mdash;')+'</span></div>';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Etapa</div><span style="color:#ccc;">'+l.etapa+'</span></div>';
html+='<div><div style="color:#556677;font-size:9px;text-transform:uppercase;margin-bottom:2px;">Enc. OS</div><span style="color:#ccc;">'+(l.dataEncerramento||'&mdash;')+'</span></div>';
html+='</div></td></tr>';
}
if(linhas.length===0){
html+='<tr class="dtech-mov-row"><td colspan="11" style="text-align:center;padding:28px;color:#556677;">Nenhum registro.</td></tr>';
html+='<tr class="dtech-mov-detail" style="display:none;"><td colspan="11"></td></tr>';
}
html+='</tbody></table></div>';
} else {
/* Mobile: barra de pesquisa + cards de OS */
html+='<div class="mob-search-bar">';
html+='<input type="text" id="mov-mob-fi" class="mob-search-input" placeholder="Buscar Nº, técnico, cliente, equipamento..." oninput="movFilterCards()">';
html+='<div class="mob-search-row">';
html+='<select id="mov-mob-stf" class="mob-search-sel" onchange="movFilterCards()">';
html+='<option value="">Tipo: Todos</option>';
html+='<option value="avulso">Avulso</option>';
html+='<option value="contrato">Contrato</option>';
html+='<option value="garantia">Garantia</option>';
html+='<option value="atendimento interno">At. Interno</option>';
html+='<option value="pendente">Pendente</option>';
html+='</select>';
html+='<select id="mov-mob-sef" class="mob-search-sel" onchange="movFilterCards()">';
html+='<option value="">Execução: Todos</option>';
html+='<option value="interno">Interno</option>';
html+='<option value="externo">Externo</option>';
html+='<option value="n/i">N/I</option>';
html+='</select>';
html+='<span id="mov-mob-rc" class="mob-search-count">'+linhas.length+' reg.</span>';
html+='</div>';
html+='</div>';
html+='<div class="mov-scroll">';
for(var i=0;i<linhas.length;i++){
var l=linhas[i];
var slaNum=l.diasSla!==null?l.diasSla:-1;
var ph=l.diasSla===null?'<span style="color:#556677;">—</span>'
:(l.diasSla<=slaLimite?'<span class="dtech-badge-ok">&#10003; Dentro</span>'
:'<span class="dtech-badge-fora">&#10007; Fora</span>');
var detId='mdet-'+i;
html+='<div class="mov-card"'+
' data-processo="'+l.processo+'"'+
' data-tecnico="'+l.tecNome.replace(/"/g,'&quot;')+'"'+
' data-cliente="'+l.cliente.replace(/"/g,'&quot;')+'"'+
' data-equip="'+l.equipamento.replace(/"/g,'&quot;')+'"'+
' data-tipo="'+l.tipo.toLowerCase()+'"'+
' data-exec="'+l.tipoExecucao.toLowerCase()+'"'+
' onclick="var d=document.getElementById(\''+detId+'\');d.style.display=d.style.display===\'none\'?\'block\':\'none\';">';
html+='<div class="mov-card-chamado">'+l.processo+'</div>';
html+='<div class="mov-card-tecnico">'+l.tecNome+'</div>';
html+='<div class="mov-card-meta">'+l.cliente+' &middot; '+l.tipo+'</div>';
html+='<div class="mov-card-values">';
html+='<div class="mov-card-val"><span class="mov-card-lbl">SLA (d)</span><span style="font-size:16px;font-weight:bold;color:'+(l.diasSla===null?'#8899aa':(l.diasSla<=slaLimite?'#2ecc71':'#e74c3c'))+';">'+(l.diasSla!==null?String(l.diasSla):'—')+'</span></div>';
html+='<div class="mov-card-val"><span class="mov-card-lbl">Prazo</span>'+ph+'</div>';
html+='<div class="mov-card-val"><span class="mov-card-lbl">Fech. Etapa</span><span style="font-size:13px;color:#ccc;">'+l.dataFechamentoFmt+'</span></div>';
html+='<div class="mov-card-val"><span class="mov-card-lbl">Status</span><span style="font-size:13px;color:#ccc;">'+l.status+'</span></div>';
html+='</div>';
html+='<div id="'+detId+'" class="mov-card-detail" style="display:none;">';
html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Execu&ccedil;&atilde;o</span>'+l.tipoExecucao+'</div>';
html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Ab. Etapa</span>'+l.dataAberturaEtapaFmt+'</div>';
html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Abertura OS</span>'+l.dataAbertura+'</div>';
html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Equipamento</span>'+l.equipamento+'</div>';
if(l.numeroSerie)html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">N&ordm; S&eacute;rie</span>'+l.numeroSerie+'</div>';
if(l.solicitante)html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Solicitante</span>'+l.solicitante+'</div>';
html+='<div class="mov-card-detail-item"><span class="mov-card-detail-lbl">Etapa</span>'+l.etapa+'</div>';
html+='</div>';
html+='</div>';
}
if(linhas.length===0)html+='<div style="padding:28px;text-align:center;color:#556677;">Nenhum registro.</div>';
html+='</div>';
}
html+='</div>';
var _movCtxIA=[];
for(var _mi=0;_mi<linhas.length;_mi++){
var _l=linhas[_mi];
_movCtxIA.push({chamado:_l.processo,tecnico:_l.tecNome,cliente:_l.cliente,tipo:_l.tipo,execucao:_l.tipoExecucao,equipamento:_l.equipamento,data_abertura:_l.dataAbertura,data_abertura_etapa:_l.dataAberturaEtapaFmt,data_fechamento_etapa:_l.dataFechamentoFmt,status:_l.status,etapa:_l.etapa,sla_dias:_l.diasSla,dentro_prazo:_l.diasSla!==null?(_l.diasSla<=slaLimite):null});
}
atualizarContextoIA('movimentacao', {dt_ini:String(engine.getField('dtIni').getValue()),dt_fim:String(engine.getField('dtFim').getValue()),sla_limite:slaLimite,execucao:String(engine.getField('cmbExecucao').getValue()||'')}, [
{titulo:'Total OS',tipo:'kpi',dados:{valor:linhas.length}},
{titulo:'Dentro',tipo:'kpi',dados:{valor:slaDentro,percentual:slaTotal>0?Math.round(slaDentro/slaTotal*100):null}},
{titulo:'Fora',tipo:'kpi',dados:{valor:slaFora,percentual:slaTotal>0?Math.round(slaFora/slaTotal*100):null}},
{titulo:'Movimentação',tipo:'tabela',dados:_movCtxIA}
]);
renderHtml(html);
}
/* ── Navegação ── */
function renderView() {
var view=String(engine.getGlobalVariable('currentView')||'dashboard');
var theme=String(engine.getGlobalVariable('theme')||'');
if(theme){
var _cbRv=engine.getLayout('chatBar');
if(_cbRv){
var cbRvComp=_cbRv.getRootComposition();
if(theme==='light'){cbRvComp.removeStyleName('dash-chatbar-dark');cbRvComp.addStyleName('dash-chatbar-light');}
else if(theme==='dark'){cbRvComp.removeStyleName('dash-chatbar-light');cbRvComp.addStyleName('dash-chatbar-dark');}
}
var _caScopeRv=engine.getLayout('caScope');
if(_caScopeRv){
if(theme==='light')_caScopeRv.getRootComposition().addStyleName('theme-light');
else if(theme==='dark')_caScopeRv.getRootComposition().removeStyleName('theme-light');
}
}
var bD=engine.getWidgetController('btnDash').getButton();
var bM=engine.getWidgetController('btnMov').getButton();
if(view==='dashboard'){
bD.addStyleName('dash-nav-active');bM.removeStyleName('dash-nav-active');renderDash();
} else {
bM.addStyleName('dash-nav-active');bD.removeStyleName('dash-nav-active');renderMov();
}
}
/* ── Preview Mobile ── */
function enterMobilePreview() {
engine.setGlobalVariable('isMobile', true);
var _rl = engine.getLayout('rootLayout');
if (_rl) {
var _rlComp = _rl.getRootComposition();
_rlComp.setWidth("375px");
_rlComp.setHeight("700px");
_rlComp.addStyleName('preview-mobile-frame');
}
var _tBar = engine.getLayout('topBar');
if (_tBar) { _tBar.getRootComposition().addStyleName('mobile-topbar'); }
var _fBar = engine.getLayout('filterBar');
if (_fBar) {
var _fBarComp = _fBar.getRootComposition();
engine.setGlobalVariable('_prevFilterBarVisible', _fBarComp.isVisible());
_fBarComp.setVisible(false);
}
var _btnDev = engine.getWidgetController('btnDevPreview');
if (_btnDev) { _btnDev.getButton().setVisible(false); }
var _btnExit = engine.getWidgetController('btnExitPreview');
if (_btnExit) {
_btnExit.getButton().setVisible(true);
_btnExit.getButton().addStyleName('preview-exit-btn');
}
var _btnThMob = engine.getWidgetController('btnThemeMob');
if (_btnThMob) { _btnThMob.getButton().setVisible(true); }
var _btnThTb = engine.getWidgetController('btnTheme');
if (_btnThTb) { _btnThTb.getButton().setVisible(false); }
// Mesmo tratamento do chatBar (Pietro) aplicado no _mobileRender real —
// esconde o toggle da sidebar web e mostra o FAB do chat nativo
var _btnTogChatPrev = engine.getWidgetController('btnToggleChat');
if (_btnTogChatPrev) { _btnTogChatPrev.getButton().setVisible(false); }
var _btnFabPrev = engine.getWidgetController('btnChatFabMobile');
if (_btnFabPrev) { _btnFabPrev.getButton().setVisible(true); }
renderView();
}
function exitMobilePreview() {
engine.unsetGlobalVariable('isMobile');
var _rl = engine.getLayout('rootLayout');
if (_rl) {
var _rlComp = _rl.getRootComposition();
_rlComp.setWidth("100%");
_rlComp.setHeight("100%");
_rlComp.removeStyleName('preview-mobile-frame');
}
var _tBar = engine.getLayout('topBar');
if (_tBar) { _tBar.getRootComposition().removeStyleName('mobile-topbar'); }
var _fBar = engine.getLayout('filterBar');
if (_fBar) {
var _wasVisible = engine.getGlobalVariable('_prevFilterBarVisible');
_fBar.getRootComposition().setVisible(_wasVisible == true);
engine.unsetGlobalVariable('_prevFilterBarVisible');
}
var _btnExit = engine.getWidgetController('btnExitPreview');
if (_btnExit) {
_btnExit.getButton().removeStyleName('preview-exit-btn');
_btnExit.getButton().setVisible(false);
}
var _isDev = engine.getGlobalVariable('isDeveloper');
var _btnDev = engine.getWidgetController('btnDevPreview');
if (_btnDev) { _btnDev.getButton().setVisible(!!_isDev); }
var _btnThMob = engine.getWidgetController('btnThemeMob');
if (_btnThMob) { _btnThMob.getButton().setVisible(false); }
var _btnThTbExit = engine.getWidgetController('btnTheme');
if (_btnThTbExit) { _btnThTbExit.getButton().setVisible(true); }
// Restaura o toggle da sidebar web e esconde o FAB do chat nativo
var _btnTogChatExit = engine.getWidgetController('btnToggleChat');
if (_btnTogChatExit) { _btnTogChatExit.getButton().setVisible(true); }
var _btnFabExit = engine.getWidgetController('btnChatFabMobile');
if (_btnFabExit) { _btnFabExit.getButton().setVisible(false); }
renderView();
}
function doToggleTheme() {
var dashLib = engine.getGlobalVariable('dashLib');
var login = engine.getGlobalVariable('userLogin');
var current = String(engine.getGlobalVariable('theme') || 'dark');
var next = current === 'dark' ? 'light' : 'dark';
engine.setGlobalVariable('theme', next);
try {
var ui = Packages.com.vaadin.ui.UI.getCurrent();
if (dashLib && ui) { dashLib.applyTheme(next, ui.getPage(), null); }
if (dashLib && login) { dashLib.savePreferences(login, { dark_mode: next === 'dark' }); }
} catch(e) {}
var bT = engine.getWidgetController('btnTheme');
if (bT) { bT.getButton().setCaption(next === 'dark' ? '\u263D' : '\u2600'); }
var bM = engine.getWidgetController('btnThemeMob');
if (bM) { bM.getButton().setCaption(next === 'dark' ? '\u263D' : '\u2600'); }
var _fb = engine.getLayout('filterBar');
if (_fb) {
var fbComp = _fb.getRootComposition();
if (next === 'light') {
fbComp.removeStyleName('dash-filterbar-dark');
fbComp.addStyleName('dash-filterbar-light');
} else {
fbComp.removeStyleName('dash-filterbar-light');
fbComp.addStyleName('dash-filterbar-dark');
}
}
var _cbThm = engine.getLayout('chatBar');
if (_cbThm) {
var cbComp = _cbThm.getRootComposition();
if (next === 'light') {
cbComp.removeStyleName('dash-chatbar-dark');
cbComp.addStyleName('dash-chatbar-light');
} else {
cbComp.removeStyleName('dash-chatbar-light');
cbComp.addStyleName('dash-chatbar-dark');
}
}
var _caScopeThm = engine.getLayout('caScope');
if (_caScopeThm) {
if (next === 'light') _caScopeThm.getRootComposition().addStyleName('theme-light');
else _caScopeThm.getRootComposition().removeStyleName('theme-light');
}
try {
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"var el=document.getElementById('dtech-main');" +
"if(el){if('" + next + "'==='light')el.classList.add('theme-light');else el.classList.remove('theme-light');}"
);
} catch(e2) {}
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
function init(mapa) {
base = mapa;
if(engine.getField('dtIni').getValue()==null){
var cal=java.util.Calendar.getInstance();
var hoje=cal.getTime();
cal.set(java.util.Calendar.DAY_OF_MONTH,1);
engine.getField('dtIni').setValue(cal.getTime());
engine.getField('dtFim').setValue(hoje);
}
engine.setGlobalVariable('currentView','dashboard');
engine.setGlobalVariable('doToggleTheme', doToggleTheme);
engine.setGlobalVariable('renderView', renderView);
engine.setGlobalVariable('enterMobilePreview', enterMobilePreview);
engine.setGlobalVariable('exitMobilePreview', exitMobilePreview);
renderView();
}
]]>
</InitScript>
</ScriptWidget>
</VerticalLayout>
</Panel>
<!-- SIDEBAR AGENTE PIETRO (DIREITA) -->
<VerticalLayout id="chatBar" width="320px" height="100%" visible="false" spacing="false" margin="false" styleName="dash-chatbar-dark">
<HorizontalLayout id="chatBarHeader" width="100%" spacing="false" margin="true">
<Label contentMode="HTML" width="100%" expandRatio="1">
<value><![CDATA[<span style="font-weight:bold;font-size:13px;color:#f78259;">&#129302; Pietro</span>]]></value>
</Label>
<ButtonWidget id="btnFecharChat" caption="&#x2715;" description="Fechar Pietro" visible="false">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.getLayout('chatBar').getRootComposition().setVisible(false);
var btn = engine.getWidgetController('btnToggleChat');
if (btn) btn.getButton().setCaption('Pietro \u25C4');
}
]]>
</onClickScript>
</ButtonWidget>
</HorizontalLayout>
<VerticalLayout id="caScope" width="100%" height="100%" expandRatio="1" spacing="false" margin="false">
<VerticalLayout id="caSurface" width="100%" height="100%" expandRatio="1" spacing="false" margin="false">
<ScrollPanel id="caPanel" width="100%" height="100%" expandRatio="1">
<VerticalLayout id="caMsgs" width="100%" margin="false" spacing="false" />
</ScrollPanel>
</VerticalLayout>
</VerticalLayout>
<HorizontalLayout id="caInputBar" width="100%" spacing="true" margin="true">
<TextArea id="caTexto" width="100%" expandRatio="1" type="string" rows="2" />
<ButtonWidget id="btnChatEnviar" caption="Enviar" defaultIcon="SEND" modifierKey="CTRL" keyCode="ENTER" align="MIDDLE_CENTER">
<onClickScript language="JavaScript">
<![CDATA[
importClass(Packages.com.vaadin.ui.UI);
function run() {
var ca = engine.getGlobalVariable('chatAgente');
if (ca) ca.enviar();
UI.getCurrent().getPage().getJavaScript().execute(
"setTimeout(function(){" +
" var ta = document.querySelector('textarea');" +
" if(ta){" +
" ta.value='';" +
" ta.dispatchEvent(new Event('input', {bubbles:true}));" +
" ta.focus();" +
" }" +
"},0);"
);
}
]]>
</onClickScript>
</ButtonWidget>
</HorizontalLayout>
</VerticalLayout>
</HorizontalLayout>
<!-- Exigida pelo script IA_chat_agente_ia; o conteúdo real fica na sidebar chatBar -->
<WindowLayout id="wndChatAgente" windowHeight="200px" windowWidth="300px" windowResizable="false" windowClosable="false" windowModal="false">
<VerticalLayout width="100%" height="100%" />
</WindowLayout>
</VerticalLayout>
</components>
</form>
</panel-form>