2063 lines
154 KiB
XML
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='✕ 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(/ /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="◄ 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="📱" 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="✕ 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 ◄" 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="☽" 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="💬" 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="☽" 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 ↓ ↑ (padrão)" />
|
|
<entry key="dif_pos_neg" value="Dif ↑ ↓" />
|
|
<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 — ' + 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 </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 </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 — ' + 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;">■</span> Locação <span style="color:#e67e22;">■</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 — ' + 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;">■</span> Locação <span style="color:#e67e22;">■</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 — ' + nomeMes + ' / ' + ano + ' <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 — ' + 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 — ' + 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 — ' + 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 <span class="' + diffCls + '">' + formatBRL(diffTotal) + '</span></div>';
|
|
html += '<div style="font-size:14px;color:#8899aa;">Locação <span class="' + diffLocCls + '">' + formatBRL(diffLoc) + '</span></div>';
|
|
html += '<div style="font-size:14px;color:#8899aa;">Serviço <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 <span class="' + diffCls + '">' + formatBRL(diffTotal) + '</span></div>';
|
|
html += '<div style="font-size:12px;color:#8899aa;">Loc. <span class="' + diffLocCls + '">' + formatBRL(diffLoc) + '</span></div>';
|
|
html += '<div style="font-size:12px;color:#8899aa;">Serv. <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">► ' + 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 + ' · ' + l.tipo + (l.vig ? ' · ' + l.vig : '') + ' · ' + 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) : '—') + '</span></div>';
|
|
html += '<div class="mov-card-val"><span class="mov-card-lbl">' + lbAtual + '</span><span>' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '—') + '</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) : '—') + '</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">↧ 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ú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ê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ça" onclick="movSort(5,1)" style="cursor:pointer;user-select:none;text-align:center;">Cobrança</th>';
|
|
html += '<th data-l="VL. ' + lbAnt + '" onclick="movSort(6,1)" style="cursor:pointer;user-select:none;text-align:right;">VL. ' + lbAnt + '</th>';
|
|
html += '<th data-l="VL. ' + lbAtual + '" onclick="movSort(7,1)" style="cursor:pointer;user-select:none;text-align:right;">VL. ' + lbAtual + '</th>';
|
|
html += '<th data-l="Diferença" onclick="movSort(8,1)" style="cursor:pointer;user-select:none;text-align:right;">Diferença</th>';
|
|
html += '<th data-l="Venc. 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 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 Cobranç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;">► ' + 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 || '—') + '</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) : '—') + '</td>';
|
|
html += '<td data-v="' + l.vAtual + '" style="text-align:right">' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '—') + '</td>';
|
|
html += '<td data-v="' + l.diff + '" style="text-align:right;color:' + dCor + ';font-weight:bold;">' + (l.diff !== 0 ? formatBRL(l.diff) : '—') + '</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 || '—') : '') + '</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 || '—');
|
|
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 || '—') + '</td>';
|
|
html += '<td style="white-space:nowrap;">' + (ad.dataEnc || '—') + '</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;">🤖 Pietro</span>]]></value>
|
|
</Label>
|
|
<ButtonWidget id="btnFecharChat" caption="✕" 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;">📱 Preview Mobile (375px)</b>]]></value>
|
|
</Label>
|
|
<Label width="100%" expandRatio="1" />
|
|
<ButtonWidget id="btnCloseMobilePreview" caption="✕">
|
|
<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="◄ 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>
|