Files
jogos_matheus/.claude/skills/vitruvio-criar-dashboard-desktop/dashboard-contrato-desktop.xml
T
2026-09-23 12:29:08 -03:00

2063 lines
154 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-contratos" width="100%" height="100%">
<name>Dashboard de Contratos</name>
<description>Dashboard de MRR com evolução mensal, proporção por tipo e contratos em demonstração.</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-contratos',
titulo: 'Pietro',
nomeAgente: 'Pietro',
tema: savedTheme,
saudacao: 'Olá! Posso ajudar com análise dos dados de contratos, MRR e movimentações.',
prompt: {
persona: 'Você é Pietro, assistente de análise de dados do Dashboard de Contratos (MRR, cobrança e movimentações).',
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 contratos, MRR, cobrança e movimentações, 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) {
//engine.debug('Erro ao inicializar o agente Pietro: ' + e);
}
}
]]>
</afterFormRenderScript>-
<initScript language="JavaScript">
<![CDATA[
importClass(Packages.com.vaadin.server.FontAwesome);
var addCss = function(page) {
var style =
".dash-wrapper { background:#0f1923; padding:16px; font-family:Arial,Helvetica,sans-serif; box-sizing:border-box; width:100%; height:100%; overflow-y:auto; } " +
".dash-wrapper.mov-view { overflow:hidden; display:flex; flex-direction:column; } " +
".mov-scroll { flex:1; overflow-y:auto; min-height:0; } " +
".mov-scroll::-webkit-scrollbar { width:6px; } " +
".mov-scroll::-webkit-scrollbar-track { background:#0f1923; } " +
".mov-scroll::-webkit-scrollbar-thumb { background:#243447; border-radius:3px; } " +
".dash-kpi-row { display:flex; gap:16px; margin-bottom:16px; } " +
".dash-kpi { background:#1a2733; border-radius:6px; padding:20px 24px; flex:1; min-width:180px; } " +
".dash-kpi-label { color:#8899aa; font-size:13px; margin-bottom:6px; } " +
".dash-kpi-value { font-size:26px; font-weight:bold; color:#fff; } " +
".dash-kpi-value.kpi-loc { color:#4a9edd; } " +
".dash-kpi-value.kpi-serv { color:#e67e22; } " +
".dash-chart-row { display:flex; gap:16px; margin-bottom:16px; } " +
".dash-chart-box { background:#1a2733; border-radius:6px; padding:16px; } " +
".dash-chart-title { color:#8899aa; font-size:13px; margin-bottom:10px; } " +
".dash-diff-row { background:#1a2733; border-radius:6px; padding:10px 20px; margin-bottom:14px; display:flex; align-items:center; gap:32px; flex-wrap:wrap; } " +
".dash-diff-label { color:#556677; font-size:12px; margin-right:6px; } " +
".dash-diff-title { color:#8899aa; font-size:12px; margin-right:20px; white-space:nowrap; } " +
".dash-diff-pos { color:#2ecc71; font-weight:bold; font-size:13px; } " +
".dash-diff-neg { color:#e74c3c; font-weight:bold; font-size:13px; } " +
".dash-demo-section { background:#1a2733; border-radius:6px; padding:16px; margin-top:4px; } " +
".dash-demo-title { color:#e67e22; font-size:14px; font-weight:bold; margin-bottom:10px; } " +
".dash-aviso { padding:30px; color:#666; text-align:center; font-size:14px; background:#0f1923; } " +
"table.dash-table { border-collapse:collapse; width:100%; font-size:12px; } " +
"table.dash-table th { background:#243447; color:#8899aa; padding:6px 8px; text-align:left; border-bottom:1px solid #2a3a4a; } " +
"table.dash-table td { padding:5px 8px; color:#ccc; border-bottom:1px solid #162030; vertical-align:middle; } " +
"table.dash-table tr:hover td { background:#1e2d3d; } " +
".mov-header { background:#1a2733; border-radius:6px; padding:12px 20px; margin-bottom:12px; display:flex; align-items:center; justify-content:space-between; } " +
".mov-title { font-size:16px; font-weight:bold; color:#fff; } " +
".mov-diff-pos { color:#2ecc71; font-weight:bold; font-size:16px; } " +
".mov-diff-neg { color:#e74c3c; font-weight:bold; font-size:16px; } " +
"table.mov-table { border-collapse:collapse; width:100%; font-size:12px; font-family:Arial; } " +
"table.mov-table th { background:#243447; color:#8899aa; padding:7px 10px; text-align:left; border-bottom:1px solid #2a3a4a; white-space:nowrap; } " +
"table.mov-table td { padding:6px 10px; color:#ccc; border-bottom:1px solid #162030; } " +
"table.mov-table tr.row-novo td { background:#0d2d1a; } " +
"table.mov-table tr.row-perdido td { background:#2d0d0d; } " +
"table.mov-table tr.row-subiu td { background:#0d1f0d; } " +
"table.mov-table tr.row-caiu td { background:#1f0d0d; } " +
"table.mov-table tr:hover td { filter:brightness(1.3); } " +
".badge-cob { display:inline-block; padding:2px 8px; border-radius:10px; font-size:10px; font-weight:bold; white-space:nowrap; } " +
".badge-sem { background:#3d1010; color:#e74c3c; border:1px solid #e74c3c; } " +
".badge-gerado { background:#0d2d0d; color:#2ecc71; border:1px solid #2ecc71; } " +
".badge-recebido { background:#0d1f3d; color:#4a9edd; border:1px solid #4a9edd; } " +
".dash-kpi-value.kpi-gerado { color:#2ecc71; } " +
".dash-kpi-value.kpi-recebido { color:#4a9edd; } " +
".dash-kpi-value.kpi-sem { color:#e74c3c; } " +
"table.mov-table tr { cursor:pointer; } " +
"table.mov-table tr.row-detail td { cursor:default; background:#0a1525; padding:10px 16px; } " +
"table.mov-table tr.row-detail:hover td { filter:none; } " +
"table.adit-table { border-collapse:collapse; width:100%; font-size:11px; margin-top:2px; } " +
"table.adit-table th { background:#243447; color:#8899aa; padding:4px 8px; text-align:left; border-bottom:1px solid #2a3a4a; } " +
"table.adit-table td { padding:4px 8px; color:#aabbcc; border-bottom:1px solid #162030; vertical-align:top; } " +
".dash-kpi-sm { background:#1a2733; border-radius:6px; padding:10px 14px; flex:1; min-width:0; } " +
".dash-kpi-sm .dash-kpi-label { color:#8899aa; font-size:12px; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } " +
".dash-kpi-sm .dash-kpi-value { font-size:17px; font-weight:bold; white-space:nowrap; } " +
".kpi-avencer { color:#f39c12; } " +
".theme-light .dash-kpi-value.kpi-loc { color:#4a9edd; } " +
".theme-light .dash-kpi-value.kpi-serv { color:#e67e22; } " +
".theme-light .dash-kpi-value.kpi-gerado { color:#2ecc71; } " +
".theme-light .dash-kpi-value.kpi-recebido { color:#4a9edd; } " +
".theme-light .dash-kpi-value.kpi-sem { color:#e74c3c; } " +
".theme-light .dash-kpi-value.kpi-avencer { color:#d97706; } " +
".theme-light .dash-diff-row { background:#ffffff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .dash-diff-label { color:#94a3b8; } " +
".theme-light .dash-diff-title { color:#64748b; } " +
".theme-light .dash-demo-section { background:#ffffff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .dash-demo-title { color:#d97706; } " +
".theme-light .dash-aviso { background:#f0f2f5; color:#94a3b8; } " +
".theme-light table.dash-table th { background:#f8fafc; color:#64748b; border-bottom:1px solid #e2e8f0; } " +
".theme-light table.dash-table td { color:#334155; border-bottom:1px solid #e2e8f0; } " +
".theme-light table.dash-table tr:hover td { background:#f1f5f9; } " +
".theme-light .mov-header { background:#ffffff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .mov-title { color:#1e293b; } " +
".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:1px solid #e2e8f0; } " +
".theme-light table.mov-table tr.row-novo td { background:#dcfce7; } " +
".theme-light table.mov-table tr.row-perdido td { background:#fee2e2; } " +
".theme-light table.mov-table tr.row-subiu td { background:#d1fae5; } " +
".theme-light table.mov-table tr.row-caiu td { background:#fde8d8; } " +
".theme-light table.mov-table tr:hover td { filter:brightness(0.95); } " +
".theme-light table.mov-table tr.row-detail td { background:#f8fafc; } " +
".theme-light table.adit-table th { background:#f8fafc; color:#64748b; border-bottom:1px solid #e2e8f0; } " +
".theme-light table.adit-table td { color:#475569; border-bottom:1px solid #e2e8f0; } " +
".theme-light .dash-kpi-sm { background:#ffffff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .dash-kpi-sm .dash-kpi-label { color:#64748b; } " +
".theme-light .mov-scroll::-webkit-scrollbar-track { background:#f0f2f5; } " +
".theme-light .mov-scroll::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:3px; } " +
// Sidebar filterBar — classes server-side (mobile-safe, sem body.classList)
".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; } " +
".dash-filterbar-dark .v-filterselect-input { background:#0f1923 !important; color:#cbd5e1 !important; border-color:#243447 !important; } " +
".dash-filterbar-light .v-filterselect-input { background:#fff !important; color:#334155 !important; border-color:#cbd5e1 !important; } " +
// Mobile + preview (scoped to .mobile-view — safe to include always)
".mobile-view .dash-kpi-row { flex-direction:column; gap:8px; } " +
".mobile-view .dash-chart-row { flex-direction:column; } " +
".mobile-view .dash-kpi { min-width:unset; } " +
// dash-kpi-row-sm: exceção à regra de coluna única acima — usada só na linha dos
// 8 KPIs pequenos e homogêneos da Movimentação (Locação/Serviço/Total/Gerado
// Cobrança/Recebido/Sem Cobrança/A Vencer/Vencido), força grid de 2 colunas em vez
// de empilhar 1 por linha (ficaria comprido demais para um resumo compacto)
".mobile-view .dash-kpi-row-sm { flex-direction:row; flex-wrap:wrap; gap:8px; } " +
".mobile-view .dash-kpi-sm { flex:0 0 calc(50% - 4px); max-width:calc(50% - 4px); min-width:unset; box-sizing:border-box; } " +
// Scroll interno da tabela/cards de movimentação no mobile — precisa estar em
// addCss() (não só no bloco _mobileRender) para funcionar também no preview de dev
".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; } " +
".mov-card { background:#1a2733; border-radius:6px; padding:12px 16px; margin-bottom:8px; cursor:pointer; } " +
".mov-card.row-novo { background:#0d2d1a; } .mov-card.row-perdido { background:#2d0d0d; } " +
".mov-card.row-subiu { background:#0d1f0d; } .mov-card.row-caiu { background:#1f0d0d; } " +
".mov-card-top { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:6px; gap:8px; } " +
".mov-card-cliente { font-weight:bold; color:#fff; font-size:13px; flex:1; } " +
".mov-card-meta { font-size:11px; 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:10px; color:#8899aa; margin-bottom:2px; } " +
".mov-card-detail { border-top:1px solid #243447; margin-top:8px; padding-top:8px; } " +
".mov-card-adit-item { margin-bottom:6px; padding-bottom:6px; border-bottom:1px dashed #1e2a38; } " +
".adit-badge { display:inline-block; background:#1e3a5f; color:#4a9edd; border-radius:3px; font-size:9px; padding:0 4px; font-weight:bold; vertical-align:middle; } " +
".theme-light .mov-card { background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.08); } " +
".theme-light .mov-card.row-novo { background:#dcfce7; } .theme-light .mov-card.row-perdido { background:#fee2e2; } " +
".theme-light .mov-card.row-subiu { background:#d1fae5; } .theme-light .mov-card.row-caiu { background:#fde8d8; } " +
".theme-light .mov-card-cliente { 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; } .theme-light .mov-card-adit-item { border-bottom-color:#e2e8f0; } " +
".mob-chart-wrap { cursor:pointer; } " +
// Mobile: espaçamento — dash-wrapper fica com height auto (não 100%)
".mobile-view .dash-wrapper { height:auto !important; } " +
".mobile-view .dash-wrapper.mov-view { overflow:visible !important; height:auto !important; } " +
// Mobile: fontes maiores nos cards de resumo (KPI) no topo da movimentação
".mobile-view .dash-kpi-sm .dash-kpi-label { font-size:13px !important; } " +
".mobile-view .dash-kpi-sm .dash-kpi-value { font-size:17px !important; } " +
// Mobile: fontes maiores nos cards individuais de movimentação
".mobile-view .mov-card-cliente { font-size:15px !important; } " +
".mobile-view .mov-card-meta { font-size:13px !important; } " +
".mobile-view .mov-card-lbl { font-size:12px !important; } " +
".mobile-view .mov-diff-pos, .mobile-view .mov-diff-neg { font-size:18px !important; } " +
".mobile-view .mov-title { font-size:20px !important; } " +
// Preview inline: contrai o rootLayout a 375px como se fosse um telefone
".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; } " +
// Botão sair preview — flutua fixo no canto para ser sempre visível independente do scroll
".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; } " +
// Preview scroll: habilita scroll da página igual ao WebView mobile
"html.mob-preview-active, html.mob-preview-active body, html.mob-preview-active .v-ui { height:auto !important; overflow-y:auto !important; min-height:100vh; } " +
// Chat sidebar (direita)
".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 acima).
".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; } ";
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 _panelRoot = engine.getLayout('panelRoot');
if (_panelRoot) { _panelRoot.getRootComposition().setHeight("100%"); }
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); }
// Mostra botão de tema único no mobile (acima dos filtros)
var _btnThMob = engine.getWidgetController('btnThemeMob');
if (_btnThMob) _btnThMob.getButton().setVisible(true);
// Oculta btnTheme do topBar — no mobile só btnThemeMob (dentro da filterBar) é visível.
// Sem isso o usuário vê dois botões de tema ao mesmo tempo.
var _btnThemeTbR = engine.getWidgetController('btnTheme');
if (_btnThemeTbR) { _btnThemeTbR.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 —
// simular mobile a partir do mobile não faz sentido.
if (_btnDevCtrl) { _btnDevCtrl.getButton().setVisible(_isDev && !engine.isGlobalVariableSet('_mobileRender')); }
} catch(e) {}
// Registra zoom de gráfico para mobile (funciona também no preview de dev)
page.getJavaScript().execute(
"window.mobileChartZoom=function(container){" +
"var svg=container.querySelector('svg');if(!svg)return;" +
"var ov=document.createElement('div');" +
"ov.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(10,18,28,0.97);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:12px;box-sizing:border-box;';" +
"var btn=document.createElement('button');" +
"btn.innerHTML='&#x2715; Fechar';" +
"btn.style.cssText='position:absolute;top:12px;right:12px;background:#e74c3c;color:#fff;border:none;padding:8px 16px;border-radius:4px;font-size:13px;cursor:pointer;z-index:1;';" +
"btn.onclick=function(e){e.stopPropagation();if(ov.parentNode)ov.parentNode.removeChild(ov);};" +
"var cl=svg.cloneNode(true);" +
"cl.removeAttribute('width');cl.style.cssText='width:100%;height:auto;max-height:85vh;';" +
"cl.setAttribute('preserveAspectRatio','xMidYMid meet');" +
"ov.appendChild(btn);ov.appendChild(cl);" +
"ov.onclick=function(){if(ov.parentNode)ov.parentNode.removeChild(ov);};" +
"document.body.appendChild(ov);" +
"};"
);
// Inicia com o painel de filtros 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);
// Aplica tema no filterBar via classe Vaadin server-side (funciona no mobile)
if (_filterBarInit) {
_filterBarInit.getRootComposition().addStyleName(
savedTheme === 'light' ? 'dash-filterbar-light' : 'dash-filterbar-dark'
);
}
// Passe SEMPRE null aqui, nunca o botão — dashLib.applyTheme pode sobrescrever a caption
// do botão com um texto próprio; a caption do ícone é setada manualmente logo abaixo.
dashLib.applyTheme(savedTheme, page, null);
var _btnThInit = engine.getWidgetController('btnTheme');
if (_btnThInit) { _btnThInit.getButton().setCaption(savedTheme === 'dark' ? '\u263D' : '\u2600'); }
// Sincroniza caption do botão de tema mobile com o tema carregado
var _btnThMobInit = engine.getWidgetController('btnThemeMob');
if (_btnThMobInit) _btnThMobInit.getButton().setCaption(savedTheme === 'dark' ? '\u263D' : '\u2600');
page.getJavaScript().execute(
"window.contratosApplyTheme=function(){" +
"var w=document.getElementById('contratos-main');" +
"if(!w)return;" +
"if(document.body.classList.contains('theme-light-app'))w.classList.add('theme-light');" +
"else w.classList.remove('theme-light');" +
"};"
);
// Registra função de ordenação client-side para a tabela de movimentação
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"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);" +
"var 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].getAttribute('data-v')||'';" +
" var vb=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?' \u25B2':' \u25BC'):'');}" +
"};"
);
// Registra filtro e exportação CSV client-side
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"window.movFilter=function(){" +
"var fi=document.getElementById('mov-fi');var q=fi?fi.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].getAttribute('data-v')||'').toLowerCase();" +
"var t1=(m.cells[1].getAttribute('data-v')||'').toLowerCase();" +
"var sp5=m.cells[5]?m.cells[5].querySelector('span'):null;" +
"var t5=sp5?(sp5.textContent||'').toLowerCase():'';" +
"var t9=m.cells[9]?(m.cells[9].textContent||'').trim().toLowerCase():'';" +
"var t10=m.cells[10]?(m.cells[10].textContent||'').trim().toLowerCase():'';" +
"var ok=!q||t0.indexOf(q)>=0||t1.indexOf(q)>=0||t5.indexOf(q)>=0||t9.indexOf(q)>=0||t10.indexOf(q)>=0;" +
"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],ths=tbl.tHead.rows[0].cells,i,j;" +
"var h=[];for(i=0;i<ths.length;i++)h.push((ths[i].getAttribute('data-l')||'').replace(/&nbsp;/g,' ').trim());" +
"var rows=tb.rows,csv=[h.join(';')];" +
"for(i=0;i<rows.length;i+=2){" +
"var row=rows[i];if(row.style.display==='none')continue;" +
"var cells=row.cells,rd=[];" +
"for(j=0;j<cells.length;j++){" +
"var v;" +
"if(j===0){v=cells[j].getAttribute('data-v')||'';}" +
"else if(j===5){var sp=cells[j].querySelector('span');v=sp?sp.textContent.trim():'';}" +
"else{v=cells[j].textContent.trim();}" +
"rd.push(v.replace(/;/g,','));" +
"}" +
"csv.push(rd.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='movimentacao.csv';" +
"document.body.appendChild(a);a.click();document.body.removeChild(a);" +
"URL.revokeObjectURL(url);" +
"};"
);
// Sincroniza tema do chatBar antes de inicializar o agente
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 + 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 id="lblSpacer" 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 escuro / claro" visible="false" width="100%">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('doToggleTheme');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<DBComboBox id="cmbAno" type="string" caption="Ano" width="100%" allowNullSelection="false">
<datasource>
<freeQuery connection-key="vitruvio_producao">
<![CDATA[
SELECT CAST(y AS VARCHAR) AS ANO_KEY, CAST(y AS VARCHAR) AS ANO_VAL
FROM generate_series(
EXTRACT(YEAR FROM CURRENT_DATE)::int - 5,
EXTRACT(YEAR FROM CURRENT_DATE)::int + 2
) AS y
ORDER BY y
]]>
</freeQuery>
</datasource>
<key-field>ANO_KEY</key-field>
<caption-field>ANO_VAL</caption-field>
<events>
<valueChange>
<script language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</script>
</valueChange>
</events>
</DBComboBox>
<ComboBox id="cmbMes" caption="Mês" width="100%" allowNullSelection="false">
<entry key="1" value="Janeiro" />
<entry key="2" value="Fevereiro" />
<entry key="3" value="Março" />
<entry key="4" value="Abril" />
<entry key="5" value="Maio" />
<entry key="6" value="Junho" />
<entry key="7" value="Julho" />
<entry key="8" value="Agosto" />
<entry key="9" value="Setembro" />
<entry key="10" value="Outubro" />
<entry key="11" value="Novembro" />
<entry key="12" value="Dezembro" />
<events>
<valueChange>
<script language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</script>
</valueChange>
</events>
</ComboBox>
<ComboBox id="cmbOrdem" caption="Ordenação" width="100%" allowNullSelection="false" visible="false">
<entry key="dif_neg_pos" value="Dif &#x2193; &#x2191; (padrão)" />
<entry key="dif_pos_neg" value="Dif &#x2191; &#x2193;" />
<entry key="sem_cobranca" value="Sem Cobrança" />
<events>
<valueChange>
<script language="JavaScript">
<![CDATA[
function run() {
var fn = engine.getGlobalVariable('renderView');
if (fn) fn();
}
]]>
</script>
</valueChange>
</events>
</ComboBox>
<!-- spacer empurra campos para o topo no Vaadin VerticalLayout -->
<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="scriptDashboard" height="100%" expandRatio="1">
<factory>
<LayoutFactory type="VerticalLayout" width="100%" height="100%" />
</factory>
<InitScript language="JavaScript">
<![CDATA[
importClass(Packages.com.vaadin.ui.Alignment);
var components = libService.loadScript('vaadinComponents');
var db = libService.loadScript('db');
var banco = new db(db.VITRUVIO_DATASOURCE);
var base = null;
var NOMES_MESES = ['','Janeiro','Fevereiro','Marco','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
var ABREV_MESES = ['','JAN','FEV','MAR','ABR','MAI','JUN','JUL','AGO','SET','OUT','NOV','DEZ'];
// ----------------------------------------------------------------
// Utilitários
// ----------------------------------------------------------------
function formatBRL(valor) {
if (valor == null || valor === '') return 'R$ 0,00';
var n = Number(valor);
var neg = n < 0;
var cents = Math.round(Math.abs(n) * 100);
var dec = String(cents % 100);
if (dec.length < 2) dec = '0' + dec;
var intStr = String(Math.floor(cents / 100));
var fmt = '', c = 0;
for (var i = intStr.length - 1; i >= 0; i--) {
if (c > 0 && c % 3 === 0) fmt = '.' + fmt;
fmt = intStr[i] + fmt;
c++;
}
var result = (neg ? '-R$ ' : 'R$ ') + fmt;
result += ',' + dec;
return result;
}
function formatBRLShort(valor) {
var n = Number(valor);
var abs = Math.abs(n), prefix = n < 0 ? '-' : '';
if (abs >= 1000000) return prefix + 'R$' + (abs / 1000000).toFixed(1) + 'M';
if (abs >= 1000) return prefix + 'R$' + Math.round(abs / 1000) + 'K';
return formatBRL(n);
}
// ----------------------------------------------------------------
// SVG: Gráfico de linha
// ----------------------------------------------------------------
function svgLineChart(dadosMeses, mesSel, theme) {
var isLight = theme === 'light';
var colorGrid = isLight ? '#e2e8f0' : '#243447';
var colorLbl = isLight ? '#94a3b8' : '#556677';
var colorSelMth = isLight ? '#1e293b' : '#ffffff';
var colorValLbl = isLight ? '#475569' : '#aabbcc';
var W = 760, H = 280;
var pL = 72, pR = 20, pT = 28, pB = 42;
var pw = W - pL - pR, ph = H - pT - pB;
var xStep = pw / 11;
var maxVal = 0;
for (var i = 0; i < dadosMeses.length; i++) {
if (dadosMeses[i].total > maxVal) maxVal = dadosMeses[i].total;
}
if (maxVal === 0) maxVal = 1;
maxVal = maxVal * 1.15;
var svg = '<svg width="100%" viewBox="0 0 ' + W + ' ' + H + '">';
for (var g = 0; g <= 4; g++) {
var gy = pT + (ph / 4) * g;
svg += '<line x1="' + pL + '" y1="' + gy + '" x2="' + (W - pR) + '" y2="' + gy + '" stroke="' + colorGrid + '" stroke-width="1"/>';
svg += '<text x="' + (pL - 5) + '" y="' + (gy + 4) + '" text-anchor="end" font-family="Arial" font-size="9" fill="' + colorLbl + '">' + formatBRLShort(maxVal * (1 - g / 4)) + '</text>';
}
svg += '<line x1="' + pL + '" y1="' + (H - pB) + '" x2="' + (W - pR) + '" y2="' + (H - pB) + '" stroke="' + colorGrid + '" stroke-width="1"/>';
var ptMap = {};
for (var i = 0; i < dadosMeses.length; i++) ptMap[dadosMeses[i].mes] = dadosMeses[i].total;
var pts = [];
for (var m = 1; m <= 12; m++) {
if (ptMap[m] !== undefined) {
pts.push({
x: pL + (m - 1) * xStep,
y: pT + ph * (1 - ptMap[m] / maxVal),
mes: m, total: ptMap[m]
});
}
}
if (pts.length > 0) {
var area = 'M ' + pts[0].x + ' ' + (H - pB);
for (var i = 0; i < pts.length; i++) area += ' L ' + pts[i].x + ' ' + pts[i].y;
area += ' L ' + pts[pts.length - 1].x + ' ' + (H - pB) + ' Z';
svg += '<path d="' + area + '" fill="#4a9edd" fill-opacity="0.10"/>';
var lp = '';
for (var i = 0; i < pts.length; i++) lp += pts[i].x + ',' + pts[i].y + ' ';
svg += '<polyline points="' + lp + '" fill="none" stroke="#4a9edd" stroke-width="2.5" stroke-linejoin="round"/>';
}
for (var m = 1; m <= 12; m++) {
var mx = pL + (m - 1) * xStep;
var isSel = (m === mesSel);
svg += '<text x="' + mx + '" y="' + (H - pB + 15) + '" text-anchor="middle" font-family="Arial" font-size="10" fill="' + (isSel ? colorSelMth : colorLbl) + '" font-weight="' + (isSel ? 'bold' : 'normal') + '">' + ABREV_MESES[m] + '</text>';
if (ptMap[m] !== undefined) {
var px = mx, py = pT + ph * (1 - ptMap[m] / maxVal);
svg += '<g>';
svg += '<circle cx="' + px + '" cy="' + py + '" r="' + (isSel ? 5 : 3.5) + '" fill="' + (isSel ? colorSelMth : '#4a9edd') + '" stroke="#4a9edd" stroke-width="' + (isSel ? 2 : 0) + '"/>';
svg += '<text x="' + px + '" y="' + (py - 8) + '" text-anchor="middle" font-family="Arial" font-size="9" fill="' + (isSel ? colorSelMth : colorValLbl) + '">' + formatBRLShort(ptMap[m]) + '</text>';
svg += '<circle cx="' + px + '" cy="' + py + '" r="12" fill="transparent"><title>' + ABREV_MESES[m] + ': ' + formatBRL(ptMap[m]) + '</title></circle>';
svg += '</g>';
}
}
svg += '</svg>';
return svg;
}
// ----------------------------------------------------------------
// SVG: Donut de proporção
// ----------------------------------------------------------------
function svgDonut(loc, serv, theme) {
var isLight = theme === 'light';
var holeFill = isLight ? '#ffffff' : '#1a2733';
var pctColor = isLight ? '#1e293b' : '#ffffff';
var noDataClr = isLight ? '#94a3b8' : '#556677';
var total = loc + serv;
if (total === 0) return '<div style="color:' + noDataClr + ';text-align:center;padding:20px;">Sem dados</div>';
var cx = 80, cy = 80, ro = 68, ri = 42;
var pLoc = loc / total, pServ = serv / total;
function arcPath(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 160 160" width="160" height="160">';
if (pLoc >= 0.9999) {
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + ro + '" fill="#4a9edd"><title>Locação: ' + formatBRL(loc) + ' (100%)</title></circle>';
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + ri + '" fill="' + holeFill + '"/>';
svg += '<text x="' + cx + '" y="' + (cy + 5) + '" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="' + pctColor + '">100%</text>';
} else if (pServ >= 0.9999) {
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + ro + '" fill="#e67e22"><title>Serviço: ' + formatBRL(serv) + ' (100%)</title></circle>';
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + ri + '" fill="' + holeFill + '"/>';
svg += '<text x="' + cx + '" y="' + (cy + 5) + '" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="' + pctColor + '">100%</text>';
} else {
var sa1 = -Math.PI / 2, ea1 = sa1 + pLoc * 2 * Math.PI;
svg += '<path d="' + arcPath(sa1, ea1) + '" fill="#4a9edd"><title>Locação: ' + formatBRL(loc) + ' (' + Math.round(pLoc * 100) + '%)</title></path>';
svg += '<path d="' + arcPath(ea1, ea1 + pServ * 2 * Math.PI) + '" fill="#e67e22"><title>Serviço: ' + formatBRL(serv) + ' (' + Math.round(pServ * 100) + '%)</title></path>';
if (pLoc > 0.07) {
var mla = sa1 + pLoc * Math.PI;
svg += '<text x="' + (cx + (ro + ri) / 2 * Math.cos(mla)) + '" y="' + (cy + (ro + ri) / 2 * Math.sin(mla) + 4) + '" text-anchor="middle" font-family="Arial" font-size="11" font-weight="bold" fill="#fff">' + Math.round(pLoc * 100) + '%</text>';
}
if (pServ > 0.07) {
var msa = ea1 + pServ * Math.PI;
svg += '<text x="' + (cx + (ro + ri) / 2 * Math.cos(msa)) + '" y="' + (cy + (ro + ri) / 2 * Math.sin(msa) + 4) + '" text-anchor="middle" font-family="Arial" font-size="11" font-weight="bold" fill="#fff">' + Math.round(pServ * 100) + '%</text>';
}
}
svg += '</svg>';
return svg;
}
function renderHtml(html) {
var isPreview = engine.isGlobalVariableSet('_previewMode');
if (isPreview) {
engine.setGlobalVariable('_capturedPreviewHtml', html);
return;
}
var comp = components.html(html);
// Mobile (WebView ou preview desktop): width only, sem setSizeFull
// Deixa base e comp com height auto → conteúdo cresce → scroll funciona
// Preview desktop: base auto-height → Panel vê conteúdo real → rola
// Mobile WebView real: setSizeFull igual ao desktop → JS colapsa tudo depois
var isPreviewMode = engine.isGlobalVariableSet('isMobile') && !engine.isGlobalVariableSet('_mobileRender');
if (isPreviewMode) {
try { base.setHeight(null); } catch(e) {}
comp.setWidth("100%");
} else {
comp.setSizeFull();
}
base.removeAllComponents();
base.addComponent(comp);
if (!isPreviewMode) {
base.setExpandRatio(comp, 1.0);
}
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"setTimeout(function(){" +
"if(typeof contratosApplyTheme==='function')contratosApplyTheme();" +
// 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('contratos-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);"
);
}
// ----------------------------------------------------------------
// Contexto IA (Pietro) — conceito padrão de painéis indicadores:
// "secoes" é um array de blocos {titulo, tipo, dados} que usa
// EXATAMENTE o mesmo texto de título exibido no HTML (KPI/gráfico/
// tabela), evitando divergência entre o que a tela mostra e o que
// o agente recebe. dados_contexto é sempre SUBSTITUÍDO por inteiro
// (nunca mesclado) a cada renderização — reflete só a view atual.
// ----------------------------------------------------------------
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
};
}
// ----------------------------------------------------------------
// View: DASHBOARD
// ----------------------------------------------------------------
function desenharDashboard() {
var anoStr = engine.getField('cmbAno').getValue();
var mesStr = engine.getField('cmbMes').getValue();
if (!anoStr || !mesStr) {
renderHtml('<div class="dash-aviso">Selecione o ano e o mês.</div>');
return;
}
var ano = Number(anoStr), mes = Number(mesStr);
var theme = String(engine.getGlobalVariable('theme') || 'dark');
var isLight = theme === 'light';
var themeClass = isLight ? ' theme-light' : '';
// KPIs do mes selecionado
var sqlKpi =
"SELECT cc.tipo_contrato AS TIPO, CAST(SUM(p.valor_parcela) AS NUMERIC(15,2)) AS TOTAL " +
"FROM vrp_contratos_parcela p " +
"JOIN vrp_contratos_capa cc ON cc.id = p.contrato_fk " +
"WHERE EXTRACT(YEAR FROM p.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato IN (1, 2) " +
"GROUP BY cc.tipo_contrato";
var locacao = 0, servico = 0;
var kpiRows = banco.query(sqlKpi, {});
if (kpiRows) {
kpiRows.each(function(row) {
var t = Number(row.TIPO), v = Number(row.TOTAL) || 0;
if (t === 1) locacao = v;
if (t === 2) servico = v;
});
}
// KPIs do mes anterior (para comparação)
var mesAnt = mes - 1, anoAnt = ano;
if (mesAnt === 0) { mesAnt = 12; anoAnt = ano - 1; }
var sqlKpiAnt =
"SELECT cc.tipo_contrato AS TIPO, CAST(SUM(p.valor_parcela) AS NUMERIC(15,2)) AS TOTAL " +
"FROM vrp_contratos_parcela p " +
"JOIN vrp_contratos_capa cc ON cc.id = p.contrato_fk " +
"WHERE EXTRACT(YEAR FROM p.data_vencimento) = " + anoAnt + " " +
" AND EXTRACT(MONTH FROM p.data_vencimento) = " + mesAnt + " " +
" AND cc.tipo_contrato IN (1, 2) " +
"GROUP BY cc.tipo_contrato";
var locacaoAnt = 0, servicoAnt = 0;
var kpiAntRows = banco.query(sqlKpiAnt, {});
if (kpiAntRows) {
kpiAntRows.each(function(row) {
var t = Number(row.TIPO), v = Number(row.TOTAL) || 0;
if (t === 1) locacaoAnt = v;
if (t === 2) servicoAnt = v;
});
}
var diffLoc = locacao - locacaoAnt;
var diffServ = servico - servicoAnt;
var diffTot = diffLoc + diffServ;
// Evolucao mensal do ano
var sqlEvol =
"SELECT CAST(EXTRACT(MONTH FROM p.data_vencimento) AS INTEGER) AS MES, " +
" CAST(SUM(p.valor_parcela) AS NUMERIC(15,2)) AS TOTAL " +
"FROM vrp_contratos_parcela p " +
"JOIN vrp_contratos_capa cc ON cc.id = p.contrato_fk " +
"WHERE EXTRACT(YEAR FROM p.data_vencimento) = " + ano + " " +
" AND cc.tipo_contrato IN (1, 2) " +
"GROUP BY CAST(EXTRACT(MONTH FROM p.data_vencimento) AS INTEGER) " +
"ORDER BY 1";
var dadosMeses = [];
var evolRows = banco.query(sqlEvol, {});
if (evolRows) {
evolRows.each(function(row) {
dadosMeses.push({ mes: Number(row.MES), total: Number(row.TOTAL) || 0 });
});
}
// Contratos em demonstracao
var sqlDemo =
"SELECT COALESCE(pe.razao_social, pe.nome) AS CLIENTE, " +
" cc.numero_contrato AS CONTRATO, CAST(SUM(p.valor_parcela) AS NUMERIC(15,2)) AS VALOR " +
"FROM vrp_contratos_parcela p " +
"JOIN vrp_contratos_capa cc ON cc.id = p.contrato_fk " +
"JOIN vrp_pessoas pe ON pe.id = cc.pessoa_fk " +
"WHERE EXTRACT(YEAR FROM p.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato = 3 " +
"GROUP BY pe.razao_social, pe.nome, cc.numero_contrato " +
"ORDER BY 1";
var demoLinhas = [], totalDemo = 0;
var demoRows = banco.query(sqlDemo, {});
if (demoRows) {
demoRows.each(function(row) {
var v = Number(row.VALOR) || 0;
totalDemo += v;
demoLinhas.push({ cliente: String(row.CLIENTE || ''), contrato: String(row.CONTRATO || ''), valor: v });
});
}
// Totais de cobrança do mês selecionado
var sqlCob =
"SELECT status_cob AS STATUS_COB, CAST(SUM(valor_parcela) AS NUMERIC(15,2)) AS TOTAL " +
"FROM ( " +
" SELECT p_parc.valor_parcela, " +
" CASE " +
" WHEN (SELECT COUNT(*) FROM vrp_cobranca_detalhe cd WHERE cd.contrato_parcela_fk = p_parc.id) = 0 THEN 'sem_cobranca' " +
" WHEN EXISTS ( " +
" SELECT 1 FROM vrp_cobranca_detalhe cd " +
" JOIN vrp_pedido ped ON ped.id = cd.pedido_gerado AND ped.situacao = 'A' " +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status = 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status = 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped.id " +
" WHERE cd.contrato_parcela_fk = p_parc.id " +
" ) THEN 'recebido' " +
" WHEN EXISTS ( " +
" SELECT 1 FROM vrp_cobranca_detalhe cd " +
" JOIN vrp_pedido ped ON ped.id = cd.pedido_gerado AND ped.situacao = 'A' " +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status <> 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status <> 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped.id " +
" WHERE cd.contrato_parcela_fk = p_parc.id " +
" ) THEN 'gerado' " +
" ELSE 'sem_cobranca' " +
" END AS status_cob " +
" FROM vrp_contratos_parcela p_parc " +
" JOIN vrp_contratos_capa cc ON cc.id = p_parc.contrato_fk " +
" WHERE EXTRACT(YEAR FROM p_parc.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p_parc.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato IN (1, 2) " +
") sub " +
"GROUP BY status_cob";
var totalGerado = 0, totalRecebido = 0, totalSemCob = 0;
var cobDashRows = banco.query(sqlCob, {});
if (cobDashRows) {
cobDashRows.each(function(row) {
var st = String(row.STATUS_COB || '');
var v = Number(row.TOTAL) || 0;
if (st === 'gerado') totalGerado = v;
if (st === 'recebido') totalRecebido = v;
if (st === 'sem_cobranca') totalSemCob = v;
});
}
// Contas a Receber: A Vencer e Vencido
var sqlCr =
"SELECT " +
" CAST(SUM(CASE WHEN p_parc.data_vencimento >= CURRENT_DATE AND EXISTS (" +
" SELECT 1 FROM vrp_cobranca_detalhe cd2" +
" JOIN vrp_pedido ped2 ON ped2.id = cd2.pedido_gerado AND ped2.situacao = 'A'" +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status <> 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status <> 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped2.id" +
" WHERE cd2.contrato_parcela_fk = p_parc.id" +
" ) THEN p_parc.valor_parcela ELSE 0 END) AS NUMERIC(15,2)) AS A_VENCER, " +
" CAST(SUM(CASE WHEN p_parc.data_vencimento < CURRENT_DATE AND EXISTS (" +
" SELECT 1 FROM vrp_cobranca_detalhe cd2" +
" JOIN vrp_pedido ped2 ON ped2.id = cd2.pedido_gerado AND ped2.situacao = 'A'" +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status <> 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status <> 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped2.id" +
" WHERE cd2.contrato_parcela_fk = p_parc.id" +
" ) THEN p_parc.valor_parcela ELSE 0 END) AS NUMERIC(15,2)) AS VENCIDO " +
"FROM vrp_contratos_parcela p_parc " +
"JOIN vrp_contratos_capa cc ON cc.id = p_parc.contrato_fk " +
"WHERE EXTRACT(YEAR FROM p_parc.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p_parc.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato IN (1, 2)";
var totalAVencer = 0, totalVencido = 0;
var crDashRows = banco.query(sqlCr, {});
if (crDashRows) {
crDashRows.each(function(row) {
totalAVencer = Number(row.A_VENCER) || 0;
totalVencido = Number(row.VENCIDO) || 0;
});
}
var nomeMes = NOMES_MESES[mes];
var mobileClass = (engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender') || engine.isGlobalVariableSet('_mobPreviewMode')) ? ' mobile-view' : '';
var html = '<div class="dash-wrapper' + themeClass + mobileClass + '" id="contratos-main">';
// KPI cards — linha 1: MRR
html += '<div class="dash-kpi-row">';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Locação</div><div class="dash-kpi-value kpi-loc">' + formatBRL(locacao) + '</div></div>';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Serviço</div><div class="dash-kpi-value kpi-serv">' + formatBRL(servico) + '</div></div>';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Total &mdash; ' + nomeMes + ' / ' + ano + '</div><div class="dash-kpi-value">' + formatBRL(locacao + servico) + '</div></div>';
html += '</div>';
// KPI cards — linha 2: situação da cobrança
html += '<div class="dash-kpi-row">';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Gerado Cobrança</div><div class="dash-kpi-value kpi-gerado">' + formatBRL(totalGerado) + '</div></div>';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Recebido</div><div class="dash-kpi-value kpi-recebido">' + formatBRL(totalRecebido) + '</div></div>';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Sem Cobrança</div><div class="dash-kpi-value kpi-sem">' + formatBRL(totalSemCob) + '</div></div>';
html += '<div class="dash-kpi"><div class="dash-kpi-label">Contas a Receber</div>';
html += '<div style="margin-top:6px;"><span style="font-size:11px;color:#8899aa;">A Vencer&nbsp;</span><span style="font-size:14px;font-weight:bold;color:#f39c12;">' + formatBRL(totalAVencer) + '</span></div>';
html += '<div style="margin-top:2px;"><span style="font-size:11px;color:#8899aa;">Vencido&nbsp;&nbsp;</span><span style="font-size:14px;font-weight:bold;color:#e74c3c;">' + formatBRL(totalVencido) + '</span></div>';
html += '</div>';
html += '</div>';
// Linha de diferença vs mês anterior
var fmtDiff = function(v) {
var cls = v >= 0 ? 'dash-diff-pos' : 'dash-diff-neg';
return '<span class="' + cls + '">' + formatBRL(v) + '</span>';
};
html += '<div class="dash-diff-row">';
html += '<span class="dash-diff-title">Diferença vs ' + NOMES_MESES[mesAnt] + '/' + String(anoAnt).slice(2) + '</span>';
html += '<span><span class="dash-diff-label">Total</span>' + fmtDiff(diffTot) + '</span>';
html += '<span><span class="dash-diff-label">Locação</span>' + fmtDiff(diffLoc) + '</span>';
html += '<span><span class="dash-diff-label">Serviço</span>' + fmtDiff(diffServ) + '</span>';
html += '</div>';
// Charts
html += '<div class="dash-chart-row">';
if (mobileClass) {
// No mobile: gráfico de linha clicável para ampliar
html += '<div class="dash-chart-box mob-chart-wrap" style="flex:4;" onclick="if(typeof mobileChartZoom===\'function\')mobileChartZoom(this)">';
html += '<div class="dash-chart-title">Evolução Mês a Mês &mdash; ' + ano + ' <span style="font-size:10px;color:#4a9edd;">(toque para ampliar)</span></div>';
html += svgLineChart(dadosMeses, mes, theme);
html += '</div>';
html += '<div class="dash-chart-box" style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;">';
html += '<div class="dash-chart-title" style="text-align:center;">Proporção</div>';
html += svgDonut(locacao, servico, theme);
html += '<div style="font-size:11px;color:#8899aa;margin-top:6px;text-align:center;"><span style="color:#4a9edd;">&#9632;</span> Locação &nbsp;<span style="color:#e67e22;">&#9632;</span> Serviço</div>';
html += '</div>';
} else {
html += '<div class="dash-chart-box" style="flex:4;">';
html += '<div class="dash-chart-title">Evolução Mês a Mês &mdash; ' + ano + '</div>';
html += svgLineChart(dadosMeses, mes, theme);
html += '</div>';
html += '<div class="dash-chart-box" style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;">';
html += '<div class="dash-chart-title" style="text-align:center;">Proporção</div>';
html += svgDonut(locacao, servico, theme);
html += '<div style="font-size:11px;color:#8899aa;margin-top:6px;text-align:center;"><span style="color:#4a9edd;">&#9632;</span> Locação &nbsp;<span style="color:#e67e22;">&#9632;</span> Serviço</div>';
html += '</div>';
}
html += '</div>';
// Demonstracao
html += '<div class="dash-demo-section">';
if (demoLinhas.length > 0) {
html += '<div class="dash-demo-title">Em Demonstração &mdash; ' + nomeMes + ' / ' + ano + ' &nbsp;<span style="color:#fff;font-size:13px;font-weight:normal;">' + formatBRL(totalDemo) + ' valorizado</span></div>';
html += '<table class="dash-table"><thead><tr><th>Cliente</th><th>Contrato</th><th style="text-align:right">Valor</th></tr></thead><tbody>';
for (var i = 0; i < demoLinhas.length; i++) {
var d = demoLinhas[i];
html += '<tr><td>' + d.cliente + '</td><td>' + d.contrato + '</td><td style="text-align:right">' + formatBRL(d.valor) + '</td></tr>';
}
var demoTotBg = isLight ? '#f1f5f9' : '#243447';
html += '<tr style="background:' + demoTotBg + ';"><td colspan="2" style="text-align:right;color:#e67e22;font-weight:bold;">Total em demonstração:</td><td style="text-align:right;color:#e67e22;font-weight:bold;">' + formatBRL(totalDemo) + '</td></tr>';
html += '</tbody></table>';
} else {
html += '<div style="color:#556677;font-size:13px;">Nenhum contrato em demonstração neste período.</div>';
}
html += '</div>';
html += '</div>';
// Contexto IA (Pietro) — secoes usam os mesmos títulos exibidos acima.
// Lista completa (mesmas linhas da tabela HTML acima, sem corte) — ver seção 7.12 do comando.
var _demoCtxIA = [];
for (var _di = 0; _di < demoLinhas.length; _di++) {
_demoCtxIA.push({ cliente: demoLinhas[_di].cliente, contrato: demoLinhas[_di].contrato, valor: demoLinhas[_di].valor });
}
atualizarContextoIA('dashboard', { ano: ano, mes: mes, nome_mes: nomeMes }, [
{ titulo: 'Locação', tipo: 'kpi', dados: { valor: locacao, valor_formatado: formatBRL(locacao) } },
{ titulo: 'Serviço', tipo: 'kpi', dados: { valor: servico, valor_formatado: formatBRL(servico) } },
{ titulo: 'Total — ' + nomeMes + ' / ' + ano, tipo: 'kpi', dados: { valor: locacao + servico, valor_formatado: formatBRL(locacao + servico) } },
{ titulo: 'Gerado Cobrança', tipo: 'kpi', dados: { valor: totalGerado, valor_formatado: formatBRL(totalGerado) } },
{ titulo: 'Recebido', tipo: 'kpi', dados: { valor: totalRecebido, valor_formatado: formatBRL(totalRecebido) } },
{ titulo: 'Sem Cobrança', tipo: 'kpi', dados: { valor: totalSemCob, valor_formatado: formatBRL(totalSemCob) } },
{ titulo: 'Contas a Receber', tipo: 'kpi_composto', dados: { a_vencer: totalAVencer, a_vencer_formatado: formatBRL(totalAVencer), vencido: totalVencido, vencido_formatado: formatBRL(totalVencido) } },
{ titulo: 'Diferença vs ' + NOMES_MESES[mesAnt] + '/' + String(anoAnt).slice(2), tipo: 'comparativo', dados: { total: diffTot, locacao: diffLoc, servico: diffServ, mes_anterior: { locacao: locacaoAnt, servico: servicoAnt, total: locacaoAnt + servicoAnt } } },
{ titulo: 'Evolução Mês a Mês — ' + ano, tipo: 'grafico_linha', dados: dadosMeses },
{ titulo: 'Proporção', tipo: 'grafico_donut', dados: { locacao: locacao, servico: servico } },
{ titulo: 'Em Demonstração — ' + nomeMes + ' / ' + ano, tipo: 'tabela', dados: { total: totalDemo, total_formatado: formatBRL(totalDemo), contratos: _demoCtxIA } }
]);
renderHtml(html);
}
// ----------------------------------------------------------------
// View: MOVIMENTAÇÃO
// ----------------------------------------------------------------
function desenharMovimentacao() {
var anoStr = engine.getField('cmbAno').getValue();
var mesStr = engine.getField('cmbMes').getValue();
if (!anoStr || !mesStr) {
renderHtml('<div class="dash-aviso">Selecione o ano e o mês.</div>');
return;
}
var ano = Number(anoStr), mes = Number(mesStr);
var theme = String(engine.getGlobalVariable('theme') || 'dark');
var isLight = theme === 'light';
var themeClass = isLight ? ' theme-light' : '';
var mesAnt = mes - 1, anoAnt = ano;
if (mesAnt === 0) { mesAnt = 12; anoAnt = ano - 1; }
var PERIODO_COBRANCA_LABEL = { '1': 'Mensal', '2': 'Bimestral', '3': 'Trimestral' };
var sqlMov =
"SELECT cc.id AS CONTRATO_ID, COALESCE(pe.razao_social, pe.nome) AS CLIENTE, cc.numero_contrato AS CONTRATO, cc.tipo_contrato AS TIPO, " +
" cc.vigencia_contrato_inicial AS VIG_INI, cc.vigencia_contrato_final AS VIG_FIM, cc.parcela AS PARCELAS, cc.periodo_cobranca AS PERIODO_COBRANCA, " +
" CAST(SUM(CASE WHEN EXTRACT(YEAR FROM p.data_vencimento) = " + anoAnt + " AND EXTRACT(MONTH FROM p.data_vencimento) = " + mesAnt + " THEN p.valor_parcela ELSE 0 END) AS NUMERIC(15,2)) AS VALOR_ANT, " +
" CAST(SUM(CASE WHEN EXTRACT(YEAR FROM p.data_vencimento) = " + ano + " AND EXTRACT(MONTH FROM p.data_vencimento) = " + mes + " THEN p.valor_parcela ELSE 0 END) AS NUMERIC(15,2)) AS VALOR_ATUAL " +
"FROM vrp_contratos_parcela p " +
"JOIN vrp_contratos_capa cc ON cc.id = p.contrato_fk " +
"JOIN vrp_pessoas pe ON pe.id = cc.pessoa_fk " +
"WHERE cc.tipo_contrato IN (1, 2) " +
" AND ((EXTRACT(YEAR FROM p.data_vencimento) = " + anoAnt + " AND EXTRACT(MONTH FROM p.data_vencimento) = " + mesAnt + ") " +
" OR (EXTRACT(YEAR FROM p.data_vencimento) = " + ano + " AND EXTRACT(MONTH FROM p.data_vencimento) = " + mes + ")) " +
"GROUP BY cc.id, pe.razao_social, pe.nome, cc.numero_contrato, cc.tipo_contrato, cc.vigencia_contrato_inicial, cc.vigencia_contrato_final, cc.parcela, cc.periodo_cobranca " +
"ORDER BY COALESCE(pe.razao_social, pe.nome), cc.numero_contrato";
var linhas = [], diffTotal = 0, diffLoc = 0, diffServ = 0;
var rows = banco.query(sqlMov, {});
if (rows) {
rows.each(function(row) {
var vA = Number(row.VALOR_ANT) || 0;
var vB = Number(row.VALOR_ATUAL) || 0;
var df = vB - vA;
diffTotal += df;
var tipo = Number(row.TIPO) || 0;
if (tipo === 1) diffLoc += df;
if (tipo === 2) diffServ += df;
var tipoLabel = tipo === 1 ? 'Locação' : (tipo === 2 ? 'Serviço' : '');
var fmtVig = function(d) {
if (!d) return '-';
var s = String(d);
return s.length >= 7 ? s.slice(5,7) + '/' + s.slice(2,4) : s;
};
var vig = fmtVig(row.VIG_INI) + ' - ' + fmtVig(row.VIG_FIM);
var periodoCobrancaKey = row.PERIODO_COBRANCA != null ? String(row.PERIODO_COBRANCA) : '';
var periodoCobrancaLabel = PERIODO_COBRANCA_LABEL[periodoCobrancaKey] || '-';
linhas.push({ id: String(row.CONTRATO_ID || ''), cliente: String(row.CLIENTE || ''), contrato: String(row.CONTRATO || ''), tipo: tipoLabel, vig: vig, parcelas: String(row.PARCELAS || ''), periodoCobranca: periodoCobrancaLabel, vAnt: vA, vAtual: vB, diff: df });
});
}
// Status de cobrança do mês atual por contrato (apenas mês selecionado)
var sqlCobMov =
"SELECT cc.numero_contrato AS CONTRATO, " +
" CASE " +
" WHEN (SELECT COUNT(*) FROM vrp_cobranca_detalhe cd WHERE cd.contrato_parcela_fk = p_parc.id) = 0 THEN 'sem_cobranca' " +
" WHEN EXISTS ( " +
" SELECT 1 FROM vrp_cobranca_detalhe cd " +
" JOIN vrp_pedido ped ON ped.id = cd.pedido_gerado AND ped.situacao = 'A' " +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status = 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status = 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped.id " +
" WHERE cd.contrato_parcela_fk = p_parc.id " +
" ) THEN 'recebido' " +
" WHEN EXISTS ( " +
" SELECT 1 FROM vrp_cobranca_detalhe cd " +
" JOIN vrp_pedido ped ON ped.id = cd.pedido_gerado AND ped.situacao = 'A' " +
" JOIN (SELECT chave_origem AS pedido_id FROM vrp_contas_receber WHERE origem = 'P' AND status <> 'B' UNION ALL SELECT vcra.chave_origem FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status <> 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped.id " +
" WHERE cd.contrato_parcela_fk = p_parc.id " +
" ) THEN 'gerado' " +
" ELSE 'sem_cobranca' " +
" END AS STATUS_COB " +
"FROM vrp_contratos_parcela p_parc " +
"JOIN vrp_contratos_capa cc ON cc.id = p_parc.contrato_fk " +
"WHERE EXTRACT(YEAR FROM p_parc.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p_parc.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato IN (1, 2)";
// Prioridade: sem_cobranca > gerado > recebido (pior caso vence quando há múltiplas parcelas)
var COB_PRIORITY = { sem_cobranca: 0, gerado: 1, recebido: 2 };
var cobMap = {};
var cobMovRows = banco.query(sqlCobMov, {});
if (cobMovRows) {
cobMovRows.each(function(row) {
var key = String(row.CONTRATO || '');
var st = String(row.STATUS_COB || 'sem_cobranca');
if (!cobMap[key] || COB_PRIORITY[st] < COB_PRIORITY[cobMap[key]]) {
cobMap[key] = st;
}
});
}
// Contas a Receber por contrato: A Vencer e Vencido (somente contratos com cobrança gerada)
var sqlCrMov =
"SELECT cc.numero_contrato AS CONTRATO, " +
" TO_CHAR(MIN(vcr.data_vencimento), 'YYYYMMDD') AS DATA_CR_ORD, " +
" TO_CHAR(MIN(vcr.data_vencimento), 'DD/MM/YYYY') AS DATA_CR_VENC, " +
" CASE WHEN MIN(vcr.data_vencimento) >= CURRENT_DATE THEN 'a_vencer' ELSE 'vencido' END AS STATUS_CR " +
"FROM vrp_contratos_parcela p_parc " +
"JOIN vrp_contratos_capa cc ON cc.id = p_parc.contrato_fk " +
"JOIN vrp_cobranca_detalhe cd ON cd.contrato_parcela_fk = p_parc.id " +
"JOIN vrp_pedido ped ON ped.id = cd.pedido_gerado AND ped.situacao = 'A' " +
"JOIN (SELECT chave_origem AS pedido_id, data_vencimento FROM vrp_contas_receber WHERE origem = 'P' AND status <> 'B' UNION ALL SELECT vcra.chave_origem, vcr2.data_vencimento FROM vrp_contas_receber_agrupado vcra JOIN vrp_contas_receber vcr2 ON vcr2.id = vcra.id_pai AND vcr2.status <> 'B' WHERE vcra.origem = 'P') vcr ON vcr.pedido_id = ped.id " +
"WHERE EXTRACT(YEAR FROM p_parc.data_vencimento) = " + ano + " " +
" AND EXTRACT(MONTH FROM p_parc.data_vencimento) = " + mes + " " +
" AND cc.tipo_contrato IN (1, 2) " +
"GROUP BY cc.numero_contrato";
var crMap = {};
var crMovRows = banco.query(sqlCrMov, {});
if (crMovRows) {
crMovRows.each(function(row) {
crMap[String(row.CONTRATO || '')] = {
dataOrd: String(row.DATA_CR_ORD || ''),
dataVenc: String(row.DATA_CR_VENC || ''),
status: String(row.STATUS_CR || '')
};
});
}
var ordemRaw = engine.getField('cmbOrdem').getValue();
var ordemVal = (ordemRaw != null) ? String(ordemRaw) : 'dif_neg_pos';
linhas.sort(function(a, b) {
if (ordemVal === 'dif_pos_neg') {
var gA = a.diff > 0 ? 0 : (a.diff < 0 ? 1 : 2);
var gB = b.diff > 0 ? 0 : (b.diff < 0 ? 1 : 2);
return gA - gB;
}
if (ordemVal === 'sem_cobranca') {
var COB_ORD = { sem_cobranca: 0, gerado: 1, recebido: 2 };
var stA = a.vAtual > 0 ? (cobMap[a.contrato] || 'sem_cobranca') : null;
var stB = b.vAtual > 0 ? (cobMap[b.contrato] || 'sem_cobranca') : null;
var oA = stA !== null ? (COB_ORD[stA] !== undefined ? COB_ORD[stA] : 3) : 4;
var oB = stB !== null ? (COB_ORD[stB] !== undefined ? COB_ORD[stB] : 3) : 4;
if (oA !== oB) return oA - oB;
}
// padrão (dif_neg_pos) e desempate das demais ordenações
return (a.diff < 0 ? 0 : (a.diff > 0 ? 1 : 2)) - (b.diff < 0 ? 0 : (b.diff > 0 ? 1 : 2));
});
// Aditivos por contrato (histórico de alterações contratuais)
var idList = [];
for (var ai = 0; ai < linhas.length; ai++) {
if (linhas[ai].id) idList.push(linhas[ai].id);
}
var aditMap = {};
if (idList.length > 0) {
var sqlAdit =
"SELECT a.contrato_fk AS CONTRATO_FK, " +
" a.local_alterado AS LOCAL_ALTERADO, a.descricao AS DESCRICAO, " +
" TO_CHAR(a.data_abertura, 'DD/MM/YYYY') AS DATA_ABERTURA, " +
" TO_CHAR(a.data_encerramento, 'DD/MM/YYYY') AS DATA_ENCERRAMENTO, " +
" a.deletar_string AS DELETAR_STRING, " +
" a.processo AS PROCESSO, " +
" pi.bpmn_process_instance AS BPMN_PROCESS_INSTANCE " +
"FROM ( " +
" SELECT a.contrato_fk, 'Contrato' AS local_alterado, ag.campo_caption AS descricao, " +
" a.data_abertura, a.data_encerramento, a.processo, " +
" 'Alterado de ' || ag.valor_antigo_info || ' para ' || ag.valor_novo_info AS deletar_string " +
" FROM vrp_aditivo a " +
" INNER JOIN vrp_aditivo_geral ag ON a.id = ag.aditivo_fk " +
" WHERE a.contrato_fk IN (" + idList.join(',') + ") " +
" UNION ALL " +
" SELECT a.contrato_fk, 'Empresa' AS local_alterado, " +
" (SELECT e.razao_social FROM vrp_pessoas e WHERE e.id = aemp.empresa_fk) AS descricao, " +
" a.data_abertura, a.data_encerramento, a.processo, " +
" CASE WHEN aemp.deletar = 1 THEN 'Deletado' WHEN aemp.deletar = 2 THEN 'Alterado' ELSE 'Inserido' END AS deletar_string " +
" FROM vrp_aditivo a " +
" INNER JOIN vrp_aditivo_empresa aemp ON a.id = aemp.aditivo_fk " +
" AND ((aemp.deletar = 0 AND aemp.contrato_empresa_fk IS NULL) OR aemp.deletar = 1) " +
" WHERE a.contrato_fk IN (" + idList.join(',') + ") " +
" UNION ALL " +
" SELECT a.contrato_fk, 'Equipamento' AS local_alterado, " +
" (SELECT pr.descricao FROM vrp_produto pr WHERE pr.id = aequip.produto_fk) AS descricao, " +
" a.data_abertura, a.data_encerramento, a.processo, " +
" CASE WHEN aequip.deletar = 1 THEN 'Deletado' WHEN aequip.deletar = 2 THEN 'Alterado' ELSE 'Inserido' END AS deletar_string " +
" FROM vrp_aditivo a " +
" INNER JOIN vrp_aditivo_equipamento aequip ON a.id = aequip.aditivo_fk " +
" AND ((aequip.deletar = 0 AND aequip.contrato_equipamento_fk IS NULL) OR aequip.deletar = 1) " +
" WHERE a.contrato_fk IN (" + idList.join(',') + ") " +
" UNION ALL " +
" SELECT a.contrato_fk, 'Produto' AS local_alterado, ap.descricao, " +
" a.data_abertura, a.data_encerramento, a.processo, " +
" CASE WHEN ap.deletar = 1 THEN 'Deletado' " +
" WHEN ap.deletar = 2 THEN 'Alterado - Vl.Unit: ' || ap.valor_unitario_prod_serv || ' p/ ' || ap.valor_unitario_prod_serv_antigo " +
" || ' | Qtd: ' || ap.quantidade_equipamentos || ' p/ ' || ap.quantidade_equipamentos_antigo " +
" || ' | Vl.NF: ' || ap.valor_nf || ' p/ ' || ap.valor_nf_antigo " +
" ELSE 'Inserido' END AS deletar_string " +
" FROM vrp_aditivo a " +
" INNER JOIN vrp_aditivo_produto ap ON a.id = ap.aditivo_fk " +
" AND ((ap.deletar = 0 AND ap.contrato_produto_fk IS NULL) OR ap.deletar = 1 OR ap.deletar = 2) " +
" WHERE a.contrato_fk IN (" + idList.join(',') + ") " +
") a " +
"LEFT JOIN processo_instancia pi ON pi.instancia_id = a.processo " +
"ORDER BY a.data_encerramento DESC, a.contrato_fk";
var aditRows = banco.query(sqlAdit, {});
if (aditRows) {
aditRows.each(function(row) {
var cid = String(row.CONTRATO_FK || '');
if (!aditMap[cid]) aditMap[cid] = [];
aditMap[cid].push({
local: String(row.LOCAL_ALTERADO || ''),
desc: String(row.DESCRICAO || ''),
dataAb: String(row.DATA_ABERTURA || ''),
dataEnc: String(row.DATA_ENCERRAMENTO || ''),
acao: String(row.DELETAR_STRING || ''),
processo: row.PROCESSO != null ? String(row.PROCESSO) : '',
bpmnPi: row.BPMN_PROCESS_INSTANCE != null ? String(row.BPMN_PROCESS_INSTANCE) : ''
});
});
}
}
var lbAnt = ABREV_MESES[mesAnt] + '/' + String(anoAnt).slice(2);
var lbAtual = ABREV_MESES[mes] + '/' + String(ano).slice(2);
var diffCls = diffTotal >= 0 ? 'mov-diff-pos' : 'mov-diff-neg';
var diffLocCls = diffLoc >= 0 ? 'mov-diff-pos' : 'mov-diff-neg';
var diffServCls = diffServ >= 0 ? 'mov-diff-pos' : 'mov-diff-neg';
// Totais do mês selecionado calculados das linhas já em memória
var movTotAtual = 0, movTotLoc = 0, movTotServ = 0;
var movTotGerado = 0, movTotRecebido = 0, movTotSemCob = 0;
var movTotAVencer = 0, movTotVencido = 0;
for (var ki = 0; ki < linhas.length; ki++) {
var lk = linhas[ki];
if (lk.vAtual > 0) {
movTotAtual += lk.vAtual;
if (lk.tipo === 'Locação') movTotLoc += lk.vAtual;
if (lk.tipo === 'Serviço') movTotServ += lk.vAtual;
var stk = cobMap[lk.contrato] || 'sem_cobranca';
if (stk === 'gerado') movTotGerado += lk.vAtual;
if (stk === 'recebido') movTotRecebido += lk.vAtual;
if (stk === 'sem_cobranca') movTotSemCob += lk.vAtual;
if (stk === 'gerado') {
var crk = crMap[lk.contrato];
if (crk) {
if (crk.status === 'a_vencer') movTotAVencer += lk.vAtual;
if (crk.status === 'vencido') movTotVencido += lk.vAtual;
}
}
}
}
// Contexto IA (Pietro) — secoes usam os mesmos títulos dos cards acima.
// "contratos" traz TODAS as linhas da tabela (mesmos dados/colunas exibidos na tela,
// já na ordem de cmbOrdem) — nunca um recorte top-N, para o Pietro conseguir responder
// qualquer pergunta sobre a movimentação (maior valor, atraso, cliente específico etc.),
// não só perguntas que coincidam com um resumo pré-calculado.
// Reconstruído aqui (antes do branch mobile/desktop) para cobrir as duas renderizações.
var _contratosIA = [];
for (var _ci = 0; _ci < linhas.length; _ci++) {
var _lc = linhas[_ci];
var _cobIA = _lc.vAtual > 0 ? (cobMap[_lc.contrato] || 'sem_cobranca') : null;
var _crIA = (_cobIA === 'gerado') ? (crMap[_lc.contrato] || null) : null;
_contratosIA.push({
cliente: _lc.cliente, contrato: _lc.contrato, tipo: _lc.tipo, vigencia: _lc.vig, parcelas: _lc.parcelas, tipo_cobranca: _lc.periodoCobranca,
cobranca: _cobIA,
valor_anterior: _lc.vAnt, valor_atual: _lc.vAtual, diferenca: _lc.diff,
cr_vencimento: _crIA ? _crIA.dataVenc : null, cr_status: _crIA ? _crIA.status : null
});
}
atualizarContextoIA('movimentacao', { ano: ano, mes: mes, ordem: ordemVal }, [
{ titulo: 'Locação — ' + lbAtual, tipo: 'kpi', dados: { valor: movTotLoc, valor_formatado: formatBRL(movTotLoc) } },
{ titulo: 'Serviço — ' + lbAtual, tipo: 'kpi', dados: { valor: movTotServ, valor_formatado: formatBRL(movTotServ) } },
{ titulo: 'Total — ' + lbAtual, tipo: 'kpi', dados: { valor: movTotAtual, valor_formatado: formatBRL(movTotAtual) } },
{ titulo: 'Gerado Cobrança', tipo: 'kpi', dados: { valor: movTotGerado, valor_formatado: formatBRL(movTotGerado) } },
{ titulo: 'Recebido', tipo: 'kpi', dados: { valor: movTotRecebido, valor_formatado: formatBRL(movTotRecebido) } },
{ titulo: 'Sem Cobrança', tipo: 'kpi', dados: { valor: movTotSemCob, valor_formatado: formatBRL(movTotSemCob) } },
{ titulo: 'A Vencer (CR)', tipo: 'kpi', dados: { valor: movTotAVencer, valor_formatado: formatBRL(movTotAVencer) } },
{ titulo: 'Vencido (CR)', tipo: 'kpi', dados: { valor: movTotVencido, valor_formatado: formatBRL(movTotVencido) } },
{ titulo: 'MOVIMENTAÇÃO', tipo: 'tabela', dados: {
periodo_anterior: lbAnt, periodo_atual: lbAtual,
variacao_total: diffTotal, variacao_locacao: diffLoc, variacao_servico: diffServ,
total_contratos: linhas.length,
contratos: _contratosIA
} }
]);
var mobileClass = (engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender') || engine.isGlobalVariableSet('_mobPreviewMode')) ? ' mobile-view' : '';
// Nota: classe "mov-view" removida — testada ao vivo no dashboard-faturamento,
// o mecanismo (overflow:hidden + flex-column no .dash-wrapper delegando scroll
// a um filho .mov-scroll) quebrou o scroll no mobile (o filho não resolve
// altura computável neste WebView). Sem ela, o wrapper usa o mesmo modelo
// simples de scroll (.dash-wrapper com overflow-y:auto) confirmado funcionando.
var html = '<div class="dash-wrapper' + themeClass + mobileClass + '" id="contratos-main">';
// Linha única com todos os totais (compacto)
// dash-kpi-row-sm: classe extra para o CSS mobile de grid de 2 colunas
// (ver addCss) — a regra genérica .mobile-view .dash-kpi-row empilha 1
// por linha, então essa linha específica precisa de override próprio
html += '<div class="dash-kpi-row dash-kpi-row-sm">';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Locação &mdash; ' + lbAtual + '</div><div class="dash-kpi-value kpi-loc">' + formatBRL(movTotLoc) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Serviço &mdash; ' + lbAtual + '</div><div class="dash-kpi-value kpi-serv">' + formatBRL(movTotServ) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Total &mdash; ' + lbAtual + '</div><div class="dash-kpi-value">' + formatBRL(movTotAtual) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Gerado Cobrança</div><div class="dash-kpi-value kpi-gerado">' + formatBRL(movTotGerado) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Recebido</div><div class="dash-kpi-value kpi-recebido">' + formatBRL(movTotRecebido) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Sem Cobrança</div><div class="dash-kpi-value kpi-sem">' + formatBRL(movTotSemCob) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">A Vencer (CR)</div><div class="dash-kpi-value kpi-avencer">' + formatBRL(movTotAVencer) + '</div></div>';
html += '<div class="dash-kpi dash-kpi-sm"><div class="dash-kpi-label">Vencido (CR)</div><div class="dash-kpi-value kpi-sem">' + formatBRL(movTotVencido) + '</div></div>';
html += '</div>';
if (mobileClass) {
// Mobile: título em cima, totais na vertical
html += '<div class="mov-header" style="flex-direction:column;align-items:flex-start;gap:6px;">';
html += '<div class="mov-title">MOVIMENTAÇÃO</div>';
html += '<div style="display:flex;flex-direction:column;gap:3px;">';
html += '<div style="font-size:14px;color:#8899aa;">Variação total&nbsp;<span class="' + diffCls + '">' + formatBRL(diffTotal) + '</span></div>';
html += '<div style="font-size:14px;color:#8899aa;">Locação&nbsp;<span class="' + diffLocCls + '">' + formatBRL(diffLoc) + '</span></div>';
html += '<div style="font-size:14px;color:#8899aa;">Serviço&nbsp;<span class="' + diffServCls + '">' + formatBRL(diffServ) + '</span></div>';
html += '</div>';
html += '</div>';
} else {
html += '<div class="mov-header">';
html += '<div class="mov-title">MOVIMENTAÇÃO</div>';
html += '<div style="display:flex;gap:32px;align-items:center;">';
html += '<div style="font-size:12px;color:#8899aa;">Variação total&nbsp;<span class="' + diffCls + '">' + formatBRL(diffTotal) + '</span></div>';
html += '<div style="font-size:12px;color:#8899aa;">Loc.&nbsp;<span class="' + diffLocCls + '">' + formatBRL(diffLoc) + '</span></div>';
html += '<div style="font-size:12px;color:#8899aa;">Serv.&nbsp;<span class="' + diffServCls + '">' + formatBRL(diffServ) + '</span></div>';
html += '</div>';
html += '</div>';
}
if (mobileClass) {
var MOB_COB = {
sem_cobranca: { cls: 'badge-sem', label: 'Sem Cobrança' },
gerado: { cls: 'badge-gerado', label: 'Gerado' },
recebido: { cls: 'badge-recebido', label: 'Recebido' }
};
html += '<div class="mov-scroll"><div class="mov-cards">';
for (var i = 0; i < linhas.length; i++) {
var l = linhas[i];
var cls = '';
if (l.vAnt === 0 && l.vAtual > 0) cls = 'row-novo';
else if (l.vAtual === 0 && l.vAnt > 0) cls = 'row-perdido';
else if (l.diff > 0) cls = 'row-subiu';
else if (l.diff < 0) cls = 'row-caiu';
var dCor = l.diff > 0 ? '#2ecc71' : (l.diff < 0 ? '#e74c3c' : (isLight ? '#94a3b8' : '#556677'));
var cobSt = l.vAtual > 0 ? (cobMap[l.contrato] || 'sem_cobranca') : null;
var cobCell = cobSt ? '<span class="badge-cob ' + MOB_COB[cobSt].cls + '">' + MOB_COB[cobSt].label + '</span>' : '';
var crDataM = (cobSt === 'gerado') ? (crMap[l.contrato] || null) : null;
var aditListM = l.id ? (aditMap[l.id] || []) : [];
var detId = 'mob-d-' + i;
var aditHint = aditListM.length > 0 ? ' <span class="adit-badge">&#9658;&nbsp;' + aditListM.length + ' adit.</span>' : '';
html += '<div class="mov-card' + (cls ? ' ' + cls : '') + '" onclick="var d=document.getElementById(this.dataset.det);if(d)d.style.display=d.style.display===\'none\'?\'block\':\'none\';" data-det="' + detId + '">';
var contratoCellM = l.contrato
? '<a href="#" onclick="event.stopPropagation();parent.vitruvio.openPanel(\'pnl_contratos\', {idPesquisaLista:\'' + l.contrato.replace(/'/g, "\\'") + '\'});return false;" style="color:#4a9edd;text-decoration:none;">' + l.contrato + '</a>'
: l.contrato;
html += '<div class="mov-card-top"><span class="mov-card-cliente">' + l.cliente + aditHint + '</span>' + cobCell + '</div>';
html += '<div class="mov-card-meta">' + contratoCellM + ' &middot; ' + l.tipo + (l.vig ? ' &middot; ' + l.vig : '') + ' &middot; ' + l.periodoCobranca + '</div>';
html += '<div class="mov-card-values">';
html += '<div class="mov-card-val"><span class="mov-card-lbl">' + lbAnt + '</span><span>' + (l.vAnt > 0 ? formatBRL(l.vAnt) : '&mdash;') + '</span></div>';
html += '<div class="mov-card-val"><span class="mov-card-lbl">' + lbAtual + '</span><span>' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '&mdash;') + '</span></div>';
html += '<div class="mov-card-val"><span class="mov-card-lbl">Dif.</span><span style="color:' + dCor + ';font-weight:bold;">' + (l.diff !== 0 ? formatBRL(l.diff) : '&mdash;') + '</span></div>';
if (crDataM) {
var crCor = crDataM.status === 'vencido' ? '#e74c3c' : '#f39c12';
var crLabel = crDataM.status === 'vencido' ? 'Vencido' : 'A Vencer';
html += '<div class="mov-card-val"><span class="mov-card-lbl">CR ' + (crDataM.dataVenc || '') + '</span><span style="color:' + crCor + ';font-weight:bold;">' + crLabel + '</span></div>';
}
html += '</div>';
html += '<div id="' + detId + '" class="mov-card-detail" style="display:none;" onclick="event.stopPropagation()">';
if (aditListM.length === 0) {
html += '<span style="color:#8899aa;font-size:11px;font-style:italic;">Nenhum aditivo registrado.</span>';
} else {
for (var aj = 0; aj < aditListM.length; aj++) {
var adm = aditListM[aj];
html += '<div class="mov-card-adit-item">';
html += '<span style="color:#4a9edd;font-size:10px;font-weight:bold;">' + (adm.local || '') + '</span>';
if (adm.desc) html += '<div style="color:#ccc;font-size:10px;margin-top:1px;">' + adm.desc + '</div>';
if (adm.acao) html += '<div style="color:#8899aa;font-size:10px;">' + adm.acao + '</div>';
if (adm.dataEnc) html += '<div style="color:#556677;font-size:9px;">Encerr: ' + adm.dataEnc + '</div>';
if (adm.processo) {
var admProcCell = adm.bpmnPi
? '<a href="#" onclick="event.stopPropagation();parent.vitruvio.showProcessData(\'' + adm.bpmnPi + '\');return false;" style="color:#4a9edd;text-decoration:none;">' + adm.processo + '</a>'
: adm.processo;
html += '<div style="color:#556677;font-size:9px;">Processo: ' + admProcCell + '</div>';
}
html += '</div>';
}
}
html += '</div>';
html += '</div>';
}
if (linhas.length === 0) {
html += '<div style="text-align:center;padding:20px;color:#556677;">Nenhum dado encontrado.</div>';
}
html += '</div></div>';
html += '</div>';
renderHtml(html);
return;
}
html += '<div class="mov-toolbar">';
html += '<input type="text" id="mov-fi" class="mov-filter-input" placeholder="Filtrar por cliente, número, cobrança (Gerado / Recebido / Sem Cobrança) ou status CR (A Vencer / Vencido)..." oninput="movFilter()">';
html += '<span id="mov-rc" style="color:#556677;font-size:11px;white-space:nowrap;"></span>';
html += '<button onclick="movExportCSV()" class="mov-export-btn">&#8615; Exportar CSV</button>';
html += '</div>';
html += '<div class="mov-scroll">';
var thS = 'style="cursor:pointer;user-select:none;"';
html += '<table id="mov-tbl" class="mov-table"><thead><tr>';
html += '<th data-l="Cliente" onclick="movSort(0,0)" ' + thS + '>Cliente</th>';
html += '<th data-l="N&uacute;mero" onclick="movSort(1,0)" ' + thS + '>Número</th>';
html += '<th data-l="Tipo" onclick="movSort(2,0)" ' + thS + '>Tipo</th>';
html += '<th data-l="Vig&ecirc;ncia" onclick="movSort(3,0)" style="cursor:pointer;user-select:none;white-space:nowrap;">Vigência</th>';
html += '<th data-l="Parc." onclick="movSort(4,1)" style="cursor:pointer;user-select:none;text-align:center;">Parc.</th>';
html += '<th data-l="Cobran&ccedil;a" onclick="movSort(5,1)" style="cursor:pointer;user-select:none;text-align:center;">Cobrança</th>';
html += '<th data-l="VL.&nbsp;' + lbAnt + '" onclick="movSort(6,1)" style="cursor:pointer;user-select:none;text-align:right;">VL. ' + lbAnt + '</th>';
html += '<th data-l="VL.&nbsp;' + lbAtual + '" onclick="movSort(7,1)" style="cursor:pointer;user-select:none;text-align:right;">VL. ' + lbAtual + '</th>';
html += '<th data-l="Diferen&ccedil;a" onclick="movSort(8,1)" style="cursor:pointer;user-select:none;text-align:right;">Diferença</th>';
html += '<th data-l="Venc.&nbsp;CR" onclick="movSort(9,0)" style="cursor:pointer;user-select:none;text-align:center;white-space:nowrap;">Venc. CR</th>';
html += '<th data-l="Status&nbsp;CR" onclick="movSort(10,0)" style="cursor:pointer;user-select:none;text-align:center;white-space:nowrap;">Status CR</th>';
html += '<th data-l="Tipo&nbsp;Cobran&ccedil;a" onclick="movSort(11,0)" style="cursor:pointer;user-select:none;text-align:center;white-space:nowrap;">Tipo Cobrança</th>';
html += '</tr></thead><tbody>';
var COB_LABEL = {
sem_cobranca: { cls: 'badge-sem', label: 'Sem Cobrança' },
gerado: { cls: 'badge-gerado', label: 'Gerado' },
recebido: { cls: 'badge-recebido', label: 'Recebido' }
};
for (var i = 0; i < linhas.length; i++) {
var l = linhas[i];
var cls = '';
if (l.vAnt === 0 && l.vAtual > 0) cls = 'row-novo';
else if (l.vAtual === 0 && l.vAnt > 0) cls = 'row-perdido';
else if (l.diff > 0) cls = 'row-subiu';
else if (l.diff < 0) cls = 'row-caiu';
var dCor = l.diff > 0 ? '#2ecc71' : (l.diff < 0 ? '#e74c3c' : (isLight ? '#94a3b8' : '#556677'));
var cobSt = l.vAtual > 0 ? (cobMap[l.contrato] || 'sem_cobranca') : null;
var cobCell = cobSt
? '<span class="badge-cob ' + COB_LABEL[cobSt].cls + '">' + COB_LABEL[cobSt].label + '</span>'
: '';
var aditList = l.id ? (aditMap[l.id] || []) : [];
var detId = 'mov-dt-' + i;
var aditBadge = aditList.length > 0
? ' <span style="display:inline-block;background:#1e3a5f;color:#4a9edd;border-radius:3px;font-size:9px;padding:0 5px;font-weight:bold;vertical-align:middle;margin-left:4px;">&#9658; ' + aditList.length + '</span>'
: '';
html += '<tr' + (cls ? ' class="' + cls + '"' : '') + ' onclick="' +
"var d=document.getElementById('" + detId + "');" +
"d.style.display=(d.style.display===''||d.style.display==='none'?'table-row':'none');" +
'">';
var cobOrd = cobSt === 'sem_cobranca' ? 0 : cobSt === 'gerado' ? 1 : cobSt === 'recebido' ? 2 : 3;
var contratoCell = l.contrato
? '<a href="#" onclick="event.stopPropagation();parent.vitruvio.openPanel(\'pnl_contratos\', {idPesquisaLista:\'' + l.contrato.replace(/'/g, "\\'") + '\'});return false;" style="color:#4a9edd;text-decoration:none;">' + l.contrato + '</a>'
: l.contrato;
html += '<td data-v="' + l.cliente.replace(/"/g, '') + '">' + l.cliente + aditBadge + '</td>';
html += '<td data-v="' + l.contrato + '">' + contratoCell + '</td>';
html += '<td data-v="' + l.tipo + '">' + l.tipo + '</td>';
html += '<td data-v="' + l.vig + '" style="white-space:nowrap;font-size:11px;color:#8899aa;">' + l.vig + '</td>';
html += '<td data-v="' + (Number(l.parcelas) || 0) + '" style="text-align:center;font-size:11px;color:#8899aa;">' + (l.parcelas || '&mdash;') + '</td>';
html += '<td data-v="' + cobOrd + '" style="text-align:center;">' + cobCell + '</td>';
html += '<td data-v="' + l.vAnt + '" style="text-align:right">' + (l.vAnt > 0 ? formatBRL(l.vAnt) : '&mdash;') + '</td>';
html += '<td data-v="' + l.vAtual + '" style="text-align:right">' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '&mdash;') + '</td>';
html += '<td data-v="' + l.diff + '" style="text-align:right;color:' + dCor + ';font-weight:bold;">' + (l.diff !== 0 ? formatBRL(l.diff) : '&mdash;') + '</td>';
var crData = (cobSt === 'gerado') ? (crMap[l.contrato] || null) : null;
html += '<td data-v="' + (crData ? crData.dataOrd : '') + '" style="text-align:center;font-size:11px;color:#8899aa;white-space:nowrap;">' + (crData ? (crData.dataVenc || '&mdash;') : '') + '</td>';
var crStatusHtml = '';
if (crData && crData.status === 'vencido') crStatusHtml = '<span style="background:#3d0d0d;color:#e74c3c;border:1px solid #e74c3c;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:bold;white-space:nowrap;">Vencido</span>';
if (crData && crData.status === 'a_vencer') crStatusHtml = '<span style="background:#3d2a00;color:#f39c12;border:1px solid #f39c12;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:bold;white-space:nowrap;">A Vencer</span>';
html += '<td data-v="' + (crData ? crData.status : '') + '" style="text-align:center;">' + crStatusHtml + '</td>';
html += '<td data-v="' + l.periodoCobranca + '" style="text-align:center;font-size:11px;color:#8899aa;white-space:nowrap;">' + l.periodoCobranca + '</td>';
html += '</tr>';
// Linha de detalhe com aditivos (oculta por padrão)
html += '<tr id="' + detId + '" class="row-detail" style="display:none;" onclick="event.stopPropagation();">';
html += '<td colspan="12">';
if (aditList.length === 0) {
html += '<span style="color:#556677;font-size:11px;">Nenhum aditivo registrado para este contrato.</span>';
} else {
html += '<table class="adit-table"><thead><tr>';
html += '<th>Tipo</th><th>Descrição</th><th>Ação</th><th>Abertura</th><th>Encerramento</th><th>Processo</th>';
html += '</tr></thead><tbody>';
for (var j = 0; j < aditList.length; j++) {
var ad = aditList[j];
var adProcCell = (ad.processo && ad.bpmnPi)
? '<a href="#" onclick="event.stopPropagation();parent.vitruvio.showProcessData(\'' + ad.bpmnPi + '\');return false;" style="color:#4a9edd;text-decoration:none;">' + ad.processo + '</a>'
: (ad.processo || '&mdash;');
html += '<tr>';
html += '<td style="white-space:nowrap;">' + ad.local + '</td>';
html += '<td>' + ad.desc + '</td>';
html += '<td>' + ad.acao + '</td>';
html += '<td style="white-space:nowrap;">' + (ad.dataAb || '&mdash;') + '</td>';
html += '<td style="white-space:nowrap;">' + (ad.dataEnc || '&mdash;') + '</td>';
html += '<td style="white-space:nowrap;">' + adProcCell + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
}
html += '</td></tr>';
}
if (linhas.length === 0) {
html += '<tr><td colspan="12" style="text-align:center;padding:20px;color:#556677;">Nenhum dado encontrado.</td></tr>';
}
html += '</tbody></table></div></div>';
renderHtml(html);
}
// ----------------------------------------------------------------
// Navegação
// ----------------------------------------------------------------
function renderView() {
var view = String(engine.getGlobalVariable('currentView') || 'dashboard');
var theme = String(engine.getGlobalVariable('theme') || '');
// Sincroniza tema do filterBar via classe Vaadin (mobile-safe)
if (theme) {
var _fbLayout = engine.getLayout('filterBar');
if (_fbLayout) {
var fbComp = _fbLayout.getRootComposition();
if (theme === 'light') {
fbComp.removeStyleName('dash-filterbar-dark');
fbComp.addStyleName('dash-filterbar-light');
} else if (theme === 'dark') {
fbComp.removeStyleName('dash-filterbar-light');
fbComp.addStyleName('dash-filterbar-dark');
}
}
// Sincroniza tema do chatBar
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');
} else {
bM.addStyleName('dash-nav-active');
bD.removeStyleName('dash-nav-active');
}
var cmbOrdem = engine.getField('cmbOrdem');
cmbOrdem.setVisible(view === 'movimentacao');
if (view === 'movimentacao' && cmbOrdem.getValue() == null) {
cmbOrdem.setValue('dif_neg_pos');
}
if (view === 'dashboard') desenharDashboard();
else desenharMovimentacao();
}
// ── Alterna tema dark/light — compartilhado por btnTheme e btnThemeMob ──
// (unifica a lógica antes duplicada nos onClickScript dos dois botões)
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');
}
}
// Sincroniza chatBar/caScope (Pietro) — este painel tem o assistente de IA
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 {
var _casThm = engine.getGlobalVariable('chatAgenteState');
if (_casThm && _casThm.config) { _casThm.config.tema = next; }
} catch(e3) {}
// Aplica a classe de tema IMEDIATAMENTE no HTML existente (sem esperar re-render)
// para eliminar o flash visual durante a troca
try {
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
"var el=document.getElementById('contratos-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 enterMobilePreview() {
engine.setGlobalVariable('isMobile', true);
// base tem height null → cresce ao tamanho do conteúdo → Panel enxerga overflow e rola
try { base.setHeight(null); } catch(e) {}
var _rl = engine.getLayout('rootLayout');
if (_rl) {
var _rlComp = _rl.getRootComposition();
_rlComp.setWidth("375px");
_rlComp.addStyleName('preview-mobile-frame');
}
var _tBar = engine.getLayout('topBar');
if (_tBar) _tBar.getRootComposition().addStyleName('mobile-topbar');
// Salva visibilidade atual do filterBar para restaurar ao sair
var _fBar = engine.getLayout('filterBar');
if (_fBar) {
var _fBarComp = _fBar.getRootComposition();
engine.setGlobalVariable('_prevFilterBarVisible', _fBarComp.isVisible());
_fBarComp.setVisible(false);
}
var _btnThMob = engine.getWidgetController('btnThemeMob');
if (_btnThMob) _btnThMob.getButton().setVisible(true);
// Oculta btnTheme do topBar — igual ao _mobileRender real, evita dois botões de tema
var _btnThTb = engine.getWidgetController('btnTheme');
if (_btnThTb) _btnThTb.getButton().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');
}
// 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');
// Restaura base para height 100% (modo desktop normal)
try { base.setHeight("100%"); } catch(e) {}
var _rl = engine.getLayout('rootLayout');
if (_rl) {
var _rlComp = _rl.getRootComposition();
_rlComp.setWidth("100%");
_rlComp.removeStyleName('preview-mobile-frame');
}
var _tBar = engine.getLayout('topBar');
if (_tBar) _tBar.getRootComposition().removeStyleName('mobile-topbar');
// Restaura filterBar ao estado exato que tinha ANTES do preview
// _wasVisible == true usa loose equality para funcionar com Java Boolean
var _fBar = engine.getLayout('filterBar');
if (_fBar) {
var _wasVisible = engine.getGlobalVariable('_prevFilterBarVisible');
_fBar.getRootComposition().setVisible(_wasVisible == true);
engine.unsetGlobalVariable('_prevFilterBarVisible');
}
var _btnThMob = engine.getWidgetController('btnThemeMob');
if (_btnThMob) _btnThMob.getButton().setVisible(false);
// Restaura btnTheme do topBar — enterMobilePreview o oculta, exit precisa reverter
var _btnThTbExit = engine.getWidgetController('btnTheme');
if (_btnThTbExit) _btnThTbExit.getButton().setVisible(true);
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);
// 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 init(mapa) {
base = mapa;
engine.setGlobalVariable('currentView', 'dashboard');
engine.setGlobalVariable('renderView', renderView);
engine.setGlobalVariable('doToggleTheme', doToggleTheme);
engine.setGlobalVariable('enterMobilePreview', enterMobilePreview);
engine.setGlobalVariable('exitMobilePreview', exitMobilePreview);
var cal = java.util.Calendar.getInstance();
engine.getField('cmbAno').setValue(String(cal.get(java.util.Calendar.YEAR)));
engine.getField('cmbMes').setValue(String(cal.get(java.util.Calendar.MONTH) + 1));
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>
<!-- Preview Mobile (apenas devs) -->
<WindowLayout id="wndMobilePreview" windowHeight="800px" windowWidth="430px"
windowResizable="true" windowClosable="true" windowModal="false">
<VerticalLayout width="100%" height="100%" margin="false" spacing="false">
<!-- Título + fechar -->
<HorizontalLayout width="100%" spacing="true" margin="true">
<Label contentMode="HTML">
<value><![CDATA[<b style="font-size:13px;">&#x1F4F1; Preview Mobile (375px)</b>]]></value>
</Label>
<Label width="100%" expandRatio="1" />
<ButtonWidget id="btnCloseMobilePreview" caption="&#x2715;">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.getLayout('wndMobilePreview').closeWindow();
}
]]>
</onClickScript>
</ButtonWidget>
</HorizontalLayout>
<!-- Barra de navegação — mesmo estilo do mobile topbar -->
<HorizontalLayout id="previewNavBar" width="375px" spacing="false" margin="false">
<ButtonWidget id="btnPreviewFiltros" caption="&#x25C4; Filtros">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
var _fr = engine.getLayout('previewFilterRow');
if (_fr) {
var _fl = _fr.getRootComposition();
_fl.setVisible(!_fl.isVisible());
}
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnPreviewDash" caption="DASHBOARD" expandRatio="1">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.setGlobalVariable('currentView', 'dashboard');
var fn = engine.getGlobalVariable('renderMobilePreview');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
<ButtonWidget id="btnPreviewMov" caption="MOVIMENTAÇÃO" expandRatio="1">
<onClickScript language="JavaScript">
<![CDATA[
function run() {
engine.setGlobalVariable('currentView', 'movimentacao');
var fn = engine.getGlobalVariable('renderMobilePreview');
if (fn) fn();
}
]]>
</onClickScript>
</ButtonWidget>
</HorizontalLayout>
<!-- Painel filtros (toggle do botão "Filtros") - mostra seleção atual -->
<HorizontalLayout id="previewFilterRow" width="375px" spacing="false" margin="false" visible="false">
<Label contentMode="HTML" width="100%">
<value><![CDATA[<div class="dash-preview-filterrow" style="width:100%;"><span id="preview-filter-info">—</span></div>]]></value>
</Label>
</HorizontalLayout>
<!-- Área de conteúdo — injetado por renderMobilePreview -->
<VerticalLayout id="previewContentArea" width="375px" height="100%" expandRatio="1" margin="false" spacing="false" />
</VerticalLayout>
</WindowLayout>
</VerticalLayout>
</components>
</form>
</panel-form>