Dashboard de Contratos Dashboard de MRR com evolução mensal, proporção por tipo e contratos em demonstração. - 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;i0?' \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=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 ANO_KEY ANO_VAL = 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 = ''; for (var g = 0; g <= 4; g++) { var gy = pT + (ph / 4) * g; svg += ''; svg += '' + formatBRLShort(maxVal * (1 - g / 4)) + ''; } svg += ''; 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 += ''; var lp = ''; for (var i = 0; i < pts.length; i++) lp += pts[i].x + ',' + pts[i].y + ' '; svg += ''; } for (var m = 1; m <= 12; m++) { var mx = pL + (m - 1) * xStep; var isSel = (m === mesSel); svg += '' + ABREV_MESES[m] + ''; if (ptMap[m] !== undefined) { var px = mx, py = pT + ph * (1 - ptMap[m] / maxVal); svg += ''; svg += ''; svg += '' + formatBRLShort(ptMap[m]) + ''; svg += '' + ABREV_MESES[m] + ': ' + formatBRL(ptMap[m]) + ''; 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 '
Sem dados
'; 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 = ''; if (pLoc >= 0.9999) { svg += 'Locação: ' + formatBRL(loc) + ' (100%)'; svg += ''; svg += '100%'; } else if (pServ >= 0.9999) { svg += 'Serviço: ' + formatBRL(serv) + ' (100%)'; svg += ''; svg += '100%'; } else { var sa1 = -Math.PI / 2, ea1 = sa1 + pLoc * 2 * Math.PI; svg += 'Locação: ' + formatBRL(loc) + ' (' + Math.round(pLoc * 100) + '%)'; svg += 'Serviço: ' + formatBRL(serv) + ' (' + Math.round(pServ * 100) + '%)'; if (pLoc > 0.07) { var mla = sa1 + pLoc * Math.PI; svg += '' + Math.round(pLoc * 100) + '%'; } if (pServ > 0.07) { var msa = ea1 + pServ * Math.PI; svg += '' + Math.round(pServ * 100) + '%'; } } 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('
Selecione o ano e o mês.
'); 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 = '
'; // KPI cards — linha 1: MRR html += '
'; html += '
Locação
' + formatBRL(locacao) + '
'; html += '
Serviço
' + formatBRL(servico) + '
'; html += '
Total — ' + nomeMes + ' / ' + ano + '
' + formatBRL(locacao + servico) + '
'; html += '
'; // KPI cards — linha 2: situação da cobrança html += '
'; html += '
Gerado Cobrança
' + formatBRL(totalGerado) + '
'; html += '
Recebido
' + formatBRL(totalRecebido) + '
'; html += '
Sem Cobrança
' + formatBRL(totalSemCob) + '
'; html += '
Contas a Receber
'; html += '
A Vencer ' + formatBRL(totalAVencer) + '
'; html += '
Vencido  ' + formatBRL(totalVencido) + '
'; html += '
'; html += '
'; // Linha de diferença vs mês anterior var fmtDiff = function(v) { var cls = v >= 0 ? 'dash-diff-pos' : 'dash-diff-neg'; return '' + formatBRL(v) + ''; }; html += '
'; html += 'Diferença vs ' + NOMES_MESES[mesAnt] + '/' + String(anoAnt).slice(2) + ''; html += 'Total' + fmtDiff(diffTot) + ''; html += 'Locação' + fmtDiff(diffLoc) + ''; html += 'Serviço' + fmtDiff(diffServ) + ''; html += '
'; // Charts html += '
'; if (mobileClass) { // No mobile: gráfico de linha clicável para ampliar html += '
'; html += '
Evolução Mês a Mês — ' + ano + ' (toque para ampliar)
'; html += svgLineChart(dadosMeses, mes, theme); html += '
'; html += '
'; html += '
Proporção
'; html += svgDonut(locacao, servico, theme); html += '
■ Locação  ■ Serviço
'; html += '
'; } else { html += '
'; html += '
Evolução Mês a Mês — ' + ano + '
'; html += svgLineChart(dadosMeses, mes, theme); html += '
'; html += '
'; html += '
Proporção
'; html += svgDonut(locacao, servico, theme); html += '
■ Locação  ■ Serviço
'; html += '
'; } html += '
'; // Demonstracao html += '
'; if (demoLinhas.length > 0) { html += '
Em Demonstração — ' + nomeMes + ' / ' + ano + '  ' + formatBRL(totalDemo) + ' valorizado
'; html += ''; for (var i = 0; i < demoLinhas.length; i++) { var d = demoLinhas[i]; html += ''; } var demoTotBg = isLight ? '#f1f5f9' : '#243447'; html += ''; html += '
ClienteContratoValor
' + d.cliente + '' + d.contrato + '' + formatBRL(d.valor) + '
Total em demonstração:' + formatBRL(totalDemo) + '
'; } else { html += '
Nenhum contrato em demonstração neste período.
'; } html += '
'; html += '
'; // 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('
Selecione o ano e o mês.
'); 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 = '
'; // 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 += '
'; html += '
Locação — ' + lbAtual + '
' + formatBRL(movTotLoc) + '
'; html += '
Serviço — ' + lbAtual + '
' + formatBRL(movTotServ) + '
'; html += '
Total — ' + lbAtual + '
' + formatBRL(movTotAtual) + '
'; html += '
Gerado Cobrança
' + formatBRL(movTotGerado) + '
'; html += '
Recebido
' + formatBRL(movTotRecebido) + '
'; html += '
Sem Cobrança
' + formatBRL(movTotSemCob) + '
'; html += '
A Vencer (CR)
' + formatBRL(movTotAVencer) + '
'; html += '
Vencido (CR)
' + formatBRL(movTotVencido) + '
'; html += '
'; if (mobileClass) { // Mobile: título em cima, totais na vertical html += '
'; html += '
MOVIMENTAÇÃO
'; html += '
'; html += '
Variação total ' + formatBRL(diffTotal) + '
'; html += '
Locação ' + formatBRL(diffLoc) + '
'; html += '
Serviço ' + formatBRL(diffServ) + '
'; html += '
'; html += '
'; } else { html += '
'; html += '
MOVIMENTAÇÃO
'; html += '
'; html += '
Variação total ' + formatBRL(diffTotal) + '
'; html += '
Loc. ' + formatBRL(diffLoc) + '
'; html += '
Serv. ' + formatBRL(diffServ) + '
'; html += '
'; html += '
'; } 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 += '
'; 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 ? '' + MOB_COB[cobSt].label + '' : ''; 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 ? ' ► ' + aditListM.length + ' adit.' : ''; html += '
'; var contratoCellM = l.contrato ? '' + l.contrato + '' : l.contrato; html += '
' + l.cliente + aditHint + '' + cobCell + '
'; html += '
' + contratoCellM + ' · ' + l.tipo + (l.vig ? ' · ' + l.vig : '') + ' · ' + l.periodoCobranca + '
'; html += '
'; html += '
' + lbAnt + '' + (l.vAnt > 0 ? formatBRL(l.vAnt) : '—') + '
'; html += '
' + lbAtual + '' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '—') + '
'; html += '
Dif.' + (l.diff !== 0 ? formatBRL(l.diff) : '—') + '
'; if (crDataM) { var crCor = crDataM.status === 'vencido' ? '#e74c3c' : '#f39c12'; var crLabel = crDataM.status === 'vencido' ? 'Vencido' : 'A Vencer'; html += '
CR ' + (crDataM.dataVenc || '') + '' + crLabel + '
'; } html += '
'; html += ''; html += '
'; } if (linhas.length === 0) { html += '
Nenhum dado encontrado.
'; } html += '
'; html += '
'; renderHtml(html); return; } html += '
'; html += ''; html += ''; html += ''; html += '
'; html += '
'; var thS = 'style="cursor:pointer;user-select:none;"'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; 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 ? '' + COB_LABEL[cobSt].label + '' : ''; var aditList = l.id ? (aditMap[l.id] || []) : []; var detId = 'mov-dt-' + i; var aditBadge = aditList.length > 0 ? ' ► ' + aditList.length + '' : ''; html += ''; var cobOrd = cobSt === 'sem_cobranca' ? 0 : cobSt === 'gerado' ? 1 : cobSt === 'recebido' ? 2 : 3; var contratoCell = l.contrato ? '' + l.contrato + '' : l.contrato; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; var crData = (cobSt === 'gerado') ? (crMap[l.contrato] || null) : null; html += ''; var crStatusHtml = ''; if (crData && crData.status === 'vencido') crStatusHtml = 'Vencido'; if (crData && crData.status === 'a_vencer') crStatusHtml = 'A Vencer'; html += ''; html += ''; html += ''; // Linha de detalhe com aditivos (oculta por padrão) html += ''; html += ''; } if (linhas.length === 0) { html += ''; } html += '
ClienteNúmeroTipoVigênciaParc.CobrançaVL. ' + lbAnt + 'VL. ' + lbAtual + 'DiferençaVenc. CRStatus CRTipo Cobrança
' + l.cliente + aditBadge + '' + contratoCell + '' + l.tipo + '' + l.vig + '' + (l.parcelas || '—') + '' + cobCell + '' + (l.vAnt > 0 ? formatBRL(l.vAnt) : '—') + '' + (l.vAtual > 0 ? formatBRL(l.vAtual) : '—') + '' + (l.diff !== 0 ? formatBRL(l.diff) : '—') + '' + (crData ? (crData.dataVenc || '—') : '') + '' + crStatusHtml + '' + l.periodoCobranca + '
Nenhum dado encontrado.
'; 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(); } ]]>