1// ======================================== 2// DASHBOARDS PAGE - JAVASCRIPT FILE 3// ======================================== 4 5// Função para criar o gráfico de rosca 6function createBudgetChart() { 7 const ctx = document.getElementById('budgetChart'); 8 if (!ctx) return; 9 10 // Destruir gráfico existente se houver 11 destroyChartIfExists('budgetChart'); 12 13 const palette = ['#AB2D2D', '#E24747', '#FB8585', '#31652B', '#67A95E', '#AFD1AA', '#422278', '#326879', '#8B4513', '#F59E0B', '#22c55e']; 14 15 function formatMillions(value) { 16 const m = value / 1_000_000; 17 return m.toFixed(1).replace('.', ',') + 'M'; 18 } 19 20 function updateBudgetLegend(labels, values) { 21 const chartCard = ctx.closest('.chart-card'); 22 if (!chartCard) return; 23 const legend = chartCard.querySelector('.chart-legend'); 24 if (!legend) return; 25 26 const legendItems = Array.from(legend.querySelectorAll('.legend-item')); 27 const count = Math.min(legendItems.length, labels.length, values.length); 28 29 for (let i = 0; i < count; i++) { 30 const item = legendItems[i]; 31 const textEl = item.querySelector('.legend-text'); 32 if (!textEl) continue; 33 const formatted = formatMillions(values[i]); 34 const fullLabel = labels[i] || ''; 35 const limit = 45; 36 const truncated = fullLabel.length > limit ? `${fullLabel.slice(0, limit)}â¦` : fullLabel; 37 textEl.innerHTML = `<span class="legend-number">${formatted}</span> ${truncated}`; 38 textEl.classList.add('has-tooltip'); 39 textEl.setAttribute('data-full', fullLabel); 40 } 41 } 42 43 function renderChart(labels, values) { 44 const backgroundColor = palette.slice(0, labels.length); 45 const chartData = { 46 labels: labels, 47 datasets: [{ 48 data: values.map(v => v / 1_000_000), 49 backgroundColor: backgroundColor, 50 borderWidth: 0, 51 cutout: '60%' 52 }] 53 }; 54 55 const config = { 56 type: 'doughnut', 57 data: chartData, 58 options: { 59 responsive: true, 60 maintainAspectRatio: false, 61 plugins: { 62 legend: { display: false }, 63 tooltip: getTooltipConfig('Distribuição de Orçamento') 64 }, 65 elements: { arc: { borderWidth: 0 } } 66 } 67 }; 68 69 new Chart(ctx, config); 70 71 const totalMillions = values.reduce((sum, v) => sum + (v / 1_000_000), 0); 72 updateChartTotal('budgetChart', Math.round(totalMillions)); 73 74 updateBudgetLegend(labels, values); 75 } 76 77 const dataUrl = '../land/public/data/orcamento_por_acao.json'; 78 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 79 80 fetch(urlWithBust, { cache: 'no-store' }) 81 .then(resp => { 82 if (!resp.ok) throw new Error('fetch not ok'); 83 return resp.json(); 84 }) 85 .then(json => { 86 const labels = json.map(item => item.descricao); 87 const values = json.map(item => Number(item.valor) || 0); 88 renderChart(labels, values); 89 }) 90 .catch(() => { 91 const labels = [ 92 'Ativos civis da união', 93 'Aposentadorias e pensões civis da união', 94 'Administração da unidade', 95 'Contribuição da união de suas autarquias e fundações para o', 96 'Concessão de bolsas para pesquisa economica', 97 'BenefÃcios obrigatorios aos servidores civis, empregados, mi', 98 'ExercÃcio da presidência dos Brics pelo brasil', 99 'Assistencia medica e odontologica aos servidores civis, empr...' 100 ]; 101 const values = [262_000_000, 253_000_000, 73_200_000, 44_600_000, 8_870_000, 3_740_000, 2_380_000, 2_280_000]; 102 renderChart(labels, values); 103 }); 104} 105 106// Tooltip HTML externo (evita ficar atrás do texto central) 107function externalDoughnutTooltipHandler(context) { 108 const { chart, tooltip } = context; 109 // Criar elemento do tooltip se não existir 110 let tooltipEl = chart.canvas.parentNode.querySelector('.chartjs-external-tooltip'); 111 if (!tooltipEl) { 112 tooltipEl = document.createElement('div'); 113 tooltipEl.className = 'chartjs-external-tooltip'; 114 tooltipEl.style.position = 'absolute'; 115 tooltipEl.style.pointerEvents = 'none'; 116 tooltipEl.style.transform = 'translate(-50%, -100%)'; 117 tooltipEl.style.zIndex = '9999'; 118 tooltipEl.style.minWidth = '120px'; 119 tooltipEl.style.maxWidth = '280px'; 120 tooltipEl.style.background = '#422278'; 121 tooltipEl.style.color = '#FFFFFF'; 122 tooltipEl.style.border = '1px solid #7A34F3'; 123 tooltipEl.style.borderRadius = '8px'; 124 tooltipEl.style.padding = '8px 10px'; 125 tooltipEl.style.fontFamily = 'Inter, sans-serif'; 126 tooltipEl.style.fontSize = '12px'; 127 tooltipEl.style.boxShadow = '0 6px 20px rgba(0,0,0,0.18)'; 128 tooltipEl.style.whiteSpace = 'nowrap'; 129 tooltipEl.style.opacity = '0'; 130 chart.canvas.parentNode.appendChild(tooltipEl); 131 } 132 133 // Esconder se não ativo 134 if (tooltip.opacity === 0) { 135 tooltipEl.style.opacity = '0'; 136 return; 137 } 138 139 // Conteúdo 140 if (tooltip.body) { 141 const titleLines = tooltip.title || []; 142 const bodyLines = tooltip.body.map(b => b.lines); 143 tooltipEl.innerHTML = ''; 144 145 const title = document.createElement('div'); 146 title.style.fontWeight = '600'; 147 title.style.marginBottom = '4px'; 148 title.textContent = titleLines.join(' '); 149 tooltipEl.appendChild(title); 150
151 bodyLines.forEach((body) => { 152 const div = document.createElement('div'); 153 div.textContent = body.join(' '); 154 tooltipEl.appendChild(div); 155 }); 156 } 157 158 // Posição 159 const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas; 160 const caret = tooltip.caretX !== undefined ? tooltip.caretX : 0; 161 const baseX = positionX + caret; 162 const baseY = positionY + (tooltip.caretY || 0) - 12; 163 164 tooltipEl.style.left = baseX + 'px'; 165 tooltipEl.style.top = baseY + 'px'; 166 tooltipEl.style.opacity = '1'; 167} 168 169// Função auxiliar para destruir gráfico existente antes de criar um novo 170function destroyChartIfExists(canvasId) { 171 const canvas = document.getElementById(canvasId); 172 if (!canvas) return; 173 174 // Verificar se há gráfico armazenado no canvas 175 if (canvas.chart) { 176 canvas.chart.destroy(); 177 canvas.chart = null; 178 } 179 180 // Verificar se Chart.js tem método getChart para obter instância existente 181 if (typeof Chart !== 'undefined' && typeof Chart.getChart === 'function') { 182 const existingChart = Chart.getChart(canvas); 183 if (existingChart) { 184 existingChart.destroy(); 185 } 186 } 187} 188 189// Função para inicializar funcionalidades especÃficas da página de dashboards 190function initDashboards() { 191 if (document.readyState === 'loading') { 192 document.addEventListener('DOMContentLoaded', () => { 193 createBudgetChart(); 194 createAposentadoriasChart(); 195 }); 196 } else { 197 createBudgetChart(); 198 createAposentadoriasChart(); 199 } 200} 201 202// Função para atualizar o total do gráfico 203function updateChartTotal(chartId, totalValue) { 204 const totalElement = document.querySelector(`#${chartId}`).parentElement.querySelector('.chart-total-number'); 205 if (totalElement) { 206 totalElement.textContent = totalValue + 'M'; 207 } 208} 209 210// Função para criar configuração padrão de tooltip 211function getTooltipConfig(title) { 212 return { 213 enabled: false, // desativa tooltip padrão (canvas) 214 external: externalDoughnutTooltipHandler, // usa HTML externo para sobrepor o centro 215 backgroundColor: '#422278', 216 titleColor: '#FFFFFF', 217 bodyColor: '#FFFFFF', 218 padding: 16, 219 cornerRadius: 8, 220 displayColors: true, 221 borderColor: '#7A34F3', 222 borderWidth: 1, 223 titleFont: { 224 size: 14, 225 weight: '600', 226 family: 'Inter' 227 }, 228 bodyFont: { 229 size: 13, 230 weight: '400', 231 family: 'Inter' 232 }, 233 callbacks: { 234 title: function(context) { 235 return title; 236 }, 237 label: function(context) { 238 const value = context.parsed; 239 if (typeof value === 'number') { 240 return 'Valor: ' + value.toLocaleString('pt-BR') + 'M'; 241 } 242 return 'Valor: ' + value; 243 } 244 } 245 }; 246} 247 248// Carregar KPIs orçamentários a partir do JSON público 249function loadBudgetKpis() { 250 const dataUrl = '../land/public/data/visao_orcamentaria_total_ipea.json'; 251 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 252 253 function formatMillions(value) { 254 const millions = (Number(value) || 0) / 1_000_000; 255 return millions.toFixed(1).replace('.', ',') + 'M'; 256 } 257 258 function setValue(id, value) { 259 const el = document.getElementById(id); 260 if (el) { 261 el.textContent = formatMillions(value); 262 } 263 } 264 265 fetch(urlWithBust, { cache: 'no-store' }) 266 .then(resp => { 267 if (!resp.ok) throw new Error('fetch not ok'); 268 return resp.json(); 269 }) 270 .then(json => { 271 const row = Array.isArray(json) ? json[0] : json; 272 if (!row) return; 273 setValue('total-budget', row.orcamento_total); 274 setValue('teds-budget', row.orcamento_recebido_teds); 275 setValue('agency-budget', row.dotacao_atualizada); 276 setValue('allocated-budget', row.orcamento_empenhado); 277 setValue('unused-budget', row.orcamento_a_liquidar); 278 setValue('programmed-expenses', row.despesas_a_pagar); 279 setValue('paid-expenses', row.despesas_pagas); 280 }) 281 .catch(() => { 282 // mantém valores existentes se falhar 283 }); 284} 285 286// Carregar KPIs de contratos a partir do JSON público 287function loadContractsKpis() { 288 const dataUrl = '../land/public/data/orcamento_contratos.json'; 289 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 290 291 function formatCurrency(value) {
292 return (Number(value) || 0).toLocaleString('pt-BR', { 293 minimumFractionDigits: 2, 294 maximumFractionDigits: 2 295 }); 296 } 297 298 function setValue(id, value) { 299 const el = document.getElementById(id); 300 if (el) { 301 el.textContent = formatCurrency(value); 302 } 303 } 304 305 fetch(urlWithBust, { cache: 'no-store' }) 306 .then(resp => { 307 if (!resp.ok) throw new Error('fetch not ok'); 308 return resp.json(); 309 }) 310 .then(json => { 311 setValue('contratos-alocado-empenhado', json.orcamento_alocado_empenhado); 312 setValue('contratos-saldo-empenho', json.saldo_de_empenho_a_liquidar); 313 setValue('contratos-despesas-pagas', json.despesas_pagas); 314 }) 315 .catch(() => { 316 // mantém valores existentes se falhar 317 }); 318} 319 320// Carregar gráfico de barras dos maiores contratos 321function loadContractsBars() { 322 const dataUrl = '../land/public/data/10_maiores_contratos_natureza_despesa.json'; 323 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 324 325 function formatValue(value) { 326 const num = Number(value) || 0; 327 if (num >= 1000000) { 328 const millions = num / 1000000; 329 // Mostrar 2 casas decimais para valores em milhões para maior precisão 330 return millions.toFixed(2).replace('.', ',') + 'M'; 331 } else if (num >= 1000) { 332 const thousands = num / 1000; 333 // Mostrar 1 casa decimal para valores em milhares 334 return thousands.toFixed(1).replace('.', ',') + 'k'; 335 } 336 return num.toLocaleString('pt-BR', { 337 minimumFractionDigits: 2, 338 maximumFractionDigits: 2 339 }); 340 } 341 342 function getNaturezaDespesa(item) { 343 // Determina qual natureza de despesa tem valor 344 if (item.locacao_de_mao_de_obra) return 'Locação de mão de obra'; 345 if (item.outros_servicos_de_terceiros_pj) return 'Outros serviços de terceiros - Pessoa JurÃdica'; 346 if (item.passagens_e_despesas_com_locomocao) return 'Passagens e despesas com locomoção'; 347 if (item.servicos_de_ti_pj) return 'Serviços de TI - Pessoa JurÃdica'; 348 return 'Não especificado'; 349 } 350 351 function getValorTotal(item) { 352 return (item.locacao_de_mao_de_obra || 0) + 353 (item.outros_servicos_de_terceiros_pj || 0) + 354 (item.passagens_e_despesas_com_locomocao || 0) + 355 (item.servicos_de_ti_pj || 0); 356 } 357 358 function getCorNatureza(natureza) { 359 const cores = { 360 'Locação de mão de obra': '#7A34F3', 361 'Outros serviços de terceiros - Pessoa JurÃdica': '#31652B', 362 'Passagens e despesas com locomoção': '#F59E0B', 363 'Serviços de TI - Pessoa JurÃdica': '#AB2D2D', 364 'Não especificado': '#326879' 365 }; 366 return cores[natureza] || '#326879'; 367 } 368 369 fetch(urlWithBust, { cache: 'no-store' }) 370 .then(resp => { 371 if (!resp.ok) throw new Error('fetch not ok'); 372 return resp.json(); 373 }) 374 .then(json => { 375 const container = document.getElementById('contracts-bars-container'); 376 if (!container) return; 377 378 // Ordenar por valor total (maior para menor) 379 const sortedData = json 380 .map(item => ({ 381 ...item, 382 valorTotal: getValorTotal(item), 383 natureza: getNaturezaDespesa(item) 384 })) 385 .sort((a, b) => b.valorTotal - a.valorTotal); 386 387 // Encontrar o valor máximo para calcular percentuais 388 const maxValue = Math.max(...sortedData.map(item => item.valorTotal)); 389 390 // Limpar container 391 container.innerHTML = ''; 392 393 // Criar barras dinamicamente 394 sortedData.forEach((item, index) => { 395 const percentage = (item.valorTotal / maxValue) * 100; 396 const formattedValue = formatValue(item.valorTotal); 397 const cor = getCorNatureza(item.natureza); 398 399 const barItem = document.createElement('div'); 400 barItem.className = 'contracts-bar-item';
401 barItem.innerHTML = ` 402 <div class="contracts-bar-header"> 403 <span class="contracts-bar-value">${formattedValue}</span> 404 <span class="contracts-bar-label">${item.fornecedor_nome}</span> 405 </div> 406 <div class="contracts-bar-track"> 407 <div class="contracts-bar-fill" style="width: ${percentage.toFixed(1)}%; background-color: ${cor};"></div> 408 </div> 409 `; 410 411 container.appendChild(barItem); 412 }); 413 }) 414 .catch(() => { 415 // mantém valores existentes se falhar 416 }); 417} 418 419// Carregar KPIs de TEDs recebidos 420function loadTedsRecebidosKpis() { 421 const dataUrl = '../land/public/data/teds_recebidos.json'; 422 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 423 424 function formatCurrency(value) { 425 return (Number(value) || 0).toLocaleString('pt-BR', { 426 minimumFractionDigits: 2, 427 maximumFractionDigits: 2 428 }); 429 } 430 431 function setValue(id, value) { 432 const el = document.getElementById(id); 433 if (el) { 434 el.textContent = value; 435 } 436 } 437 438 fetch(urlWithBust, { cache: 'no-store' }) 439 .then(resp => { 440 if (!resp.ok) throw new Error('fetch not ok'); 441 return resp.json(); 442 }) 443 .then(json => { 444 setValue('teds-recebidos-total', json.teds_recebidos); 445 setValue('teds-proximos-finalizar', json.teds_proximos_finalizar); 446 setValue('teds-despesas-liquidar', formatCurrency(json.despesas_a_liquidar_teds)); 447 }) 448 .catch(() => { 449 // mantém valores existentes se falhar 450 }); 451} 452 453// Carregar KPIs de TEDs enviados 454function loadTedsEnviadosKpis() { 455 const dataUrl = '../land/public/data/teds_enviados.json'; 456 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 457 458 function formatCurrency(value) { 459 return (Number(value) || 0).toLocaleString('pt-BR', { 460 minimumFractionDigits: 2, 461 maximumFractionDigits: 2 462 }); 463 } 464 465 function setValue(id, value) { 466 const el = document.getElementById(id); 467 if (el) { 468 el.textContent = formatCurrency(value); 469 } 470 } 471 472 fetch(urlWithBust, { cache: 'no-store' }) 473 .then(resp => { 474 if (!resp.ok) throw new Error('fetch not ok'); 475 return resp.json(); 476 }) 477 .then(json => { 478 setValue('teds-enviados-valor-firmado', json.valor_firmado); 479 setValue('teds-enviados-destaque-orcamentario', json.destaque_orcamentario_enviado); 480 }) 481 .catch((err) => { 482 console.error('Erro ao carregar KPIs de TEDs enviados:', err); 483 }); 484} 485 486 487// Função para criar os gráficos de dashboard 488function createDashboardCharts() { 489 // Gráfico 1 - Aposentadorias, Reserva Remunerada e Reformas 490 const retirementCtx = document.getElementById('retirementChart'); 491 if (retirementCtx) { 492 // Dados do gráfico 493 const retirementData = [ 494 { filled: 67, empty: 30 }, // Anel externo 495 { filled: 67, empty: 30 }, // Anel médio 496 { filled: 68, empty: 30 } // Anel interno 497 ]; 498 499 // Calcular total (soma dos valores preenchidos) 500 const retirementTotal = retirementData.reduce((sum, ring) => sum + ring.filled, 0); 501 502 new Chart(retirementCtx, { 503 type: 'doughnut', 504 data: { 505 datasets: [ 506 { 507 data: [retirementData[0].filled, retirementData[0].empty], 508 backgroundColor: ['#AA79FE', '#F7F7F7'], 509 borderWidth: 1, 510 borderColor: '#F7F7F7', 511 cutout: '60%' 512 }, 513 { 514 data: [retirementData[1].filled, retirementData[1].empty], 515 backgroundColor: ['#7A34F3', '#F7F7F7'], 516 borderWidth: 1, 517 borderColor: '#F7F7F7', 518 cutout: '60%' 519 }, 520 { 521 data: [retirementData[2].filled, retirementData[2].empty], 522 backgroundColor: ['#422278', '#F7F7F7'], 523 borderWidth: 1, 524 borderColor: '#F7F7F7', 525 cutout: '60%' 526 } 527 ] 528 }, 529 options: { 530 responsive: true, 531 maintainAspectRatio: false, 532 plugins: { 533 legend: { display: false }, 534 tooltip: getTooltipConfig('Aposentadorias e Reformas') 535 } 536 } 537 }); 538 539 // Atualizar o total automaticamente 540 updateChartTotal('retirementChart', retirementTotal); 541 } 542 543 // Gráfico 2 - Vencimento e Vantagens Fixas - Pessoa civil 544 const salaryCtx = document.getElementById('salaryChart'); 545 if (salaryCtx) { 546 // Dados do gráfico 547 const salaryData = [ 548 { filled: 87, empty: 30 }, // Anel externo 549 { filled: 87, empty: 30 }, // Anel médio 550 { filled: 86, empty: 30 } // Anel interno 551 ]; 552 553 // Calcular total (soma dos valores preenchidos) 554 const salaryTotal = salaryData.reduce((sum, ring) => sum + ring.filled, 0); 555 556 new Chart(salaryCtx, { 557 type: 'doughnut', 558 data: { 559 datasets: [ 560 { 561 data: [salaryData[0].filled, salaryData[0].empty], 562 backgroundColor: ['#AA79FE', '#FFF2FE'], 563 borderWidth: 1, 564 borderColor: '#F7F7F7', 565 cutout: '60%' 566 }, 567 { 568 data: [salaryData[1].filled, salaryData[1].empty], 569 backgroundColor: ['#7A34F3', '#FFF2FE'], 570 borderWidth: 1, 571 borderColor: '#F7F7F7', 572 cutout: '60%' 573 }, 574 { 575 data: [salaryData[2].filled, salaryData[2].empty], 576 backgroundColor: ['#422278', '#FFF2FE'], 577 borderWidth: 1, 578 borderColor: '#F7F7F7', 579 cutout: '60%' 580 } 581 ] 582 }, 583 options: { 584 responsive: true, 585 maintainAspectRatio: false, 586 plugins: { 587 legend: { display: false }, 588 tooltip: getTooltipConfig('Vencimentos e Vantagens') 589 } 590 } 591 }); 592 593 // Atualizar o total automaticamente 594 updateChartTotal('salaryChart', salaryTotal); 595 } 596 597 // Gráfico 3 - A detalhar 598 const detailCtx = document.getElementById('detailChart'); 599 if (detailCtx) { 600 // Dados do gráfico 601 const detailData = [ 602 { filled: 21, empty: 10 }, // Anel externo 603 { filled: 21, empty: 10 }, // Anel médio 604 { filled: 21, empty: 10 } // Anel interno 605 ]; 606 607 // Calcular total (soma dos valores preenchidos) 608 const detailTotal = detailData.reduce((sum, ring) => sum + ring.filled, 0); 609 610 new Chart(detailCtx, { 611 type: 'doughnut', 612 data: { 613 datasets: [ 614 { 615 data: [detailData[0].filled, detailData[0].empty], 616 backgroundColor: ['#AB2D2D', '#F7F7F7'], 617 borderWidth: 1, 618 borderColor: '#F7F7F7', 619 cutout: '60%' 620 }, 621 { 622 data: [detailData[1].filled, detailData[1].empty], 623 backgroundColor: ['#F19F42', '#F7F7F7'], 624 borderWidth: 1, 625 borderColor: '#F7F7F7', 626 cutout: '60%' 627 }, 628 { 629 data: [detailData[2].filled, detailData[2].empty], 630 backgroundColor: ['#E7D551', '#F7F7F7'], 631 borderWidth: 1, 632 borderColor: '#F7F7F7', 633 cutout: '60%' 634 } 635 ] 636 }, 637 options: { 638 responsive: true, 639 maintainAspectRatio: false, 640 plugins: { 641 legend: { display: false }, 642 tooltip: getTooltipConfig('A Detalhar') 643 } 644 } 645 }); 646 647 // Atualizar o total automaticamente 648 updateChartTotal('detailChart', detailTotal); 649 } 650 651 // Gráfico 4 - Aposentadorias, Reserva Remunerada e Reformas 652 const retirementChart2Ctx = document.getElementById('retirementChart2'); 653 if (retirementChart2Ctx) { 654 const retirementChart2Data = [ 655 { filled: 101, empty: 56 }, // Anel externo 656 { filled: 101, empty: 56 }, // Anel médio 657 ]; 658 const retirementChart2Total = retirementChart2Data.reduce((sum, ring) => sum + ring.filled, 0); 659 660 new Chart(retirementChart2Ctx, { 661 type: 'doughnut', 662 data: { 663 datasets: [ 664 { 665 data: [retirementChart2Data[0].filled, retirementChart2Data[0].empty], 666 backgroundColor: ['#AFD1AA', '#B2D1DA'], 667 borderWidth: 1, 668 borderColor: '#F7F7F7', 669 cutout: '50%' 670 }, 671 { 672 data: [retirementChart2Data[1].filled, retirementChart2Data[1].empty],
673 backgroundColor: ['#31652B', '#B2D1DA'], 674 borderWidth: 1, 675 borderColor: '#F7F7F7', 676 cutout: '60%' 677 }, 678 ] 679 }, 680 options: { 681 responsive: true, 682 maintainAspectRatio: false, 683 plugins: { 684 legend: { display: false }, 685 tooltip: getTooltipConfig('Aposentadorias e Reformas') 686 } 687 } 688 }); 689 updateChartTotal('retirementChart2', retirementChart2Total); 690 } 691 692 // Gráfico 5 - Vencimento e Vantagens Fixas - Pessoa civil 693 const salaryChart2Ctx = document.getElementById('salaryChart2'); 694 if (salaryChart2Ctx) { 695 const salaryChart2Data = [ 696 { filled: 101, empty: 56 }, // Anel externo 697 { filled: 101, empty: 56 }, // Anel médio 698 ]; 699 const salaryChart2Total = salaryChart2Data.reduce((sum, ring) => sum + ring.filled, 0); 700 701 new Chart(salaryChart2Ctx, { 702 type: 'doughnut', 703 data: { 704 datasets: [ 705 { 706 data: [salaryChart2Data[0].filled, salaryChart2Data[0].empty], 707 backgroundColor: ['#AFD1AA', '#B2D1DA'], 708 borderWidth: 1, 709 borderColor: '#F7F7F7', 710 cutout: '50%' 711 }, 712 { 713 data: [salaryChart2Data[1].filled, salaryChart2Data[1].empty], 714 backgroundColor: ['#31652B', '#B2D1DA'], 715 borderWidth: 1, 716 borderColor: '#F7F7F7', 717 cutout: '60%' 718 }, 719 ] 720 }, 721 options: { 722 responsive: true, 723 maintainAspectRatio: false, 724 plugins: { 725 legend: { display: false }, 726 tooltip: getTooltipConfig('Vencimentos e Vantagens') 727 } 728 } 729 }); 730 updateChartTotal('salaryChart2', salaryChart2Total); 731 } 732 733 // Gráfico 6 - Concessão de Bolsas para Pesquisa 734 const scholarshipCtx = document.getElementById('scholarshipChart'); 735 if (scholarshipCtx) { 736 const scholarshipData = [ 737 { filled: 101, empty: 56 }, // Anel externo 738 { filled: 101, empty: 56 }, // Anel médio 739 ]; 740 const scholarshipTotal = scholarshipData.reduce((sum, ring) => sum + ring.filled, 0); 741 742 new Chart(scholarshipCtx, { 743 type: 'doughnut', 744 data: { 745 datasets: [ 746 { 747 data: [scholarshipData[0].filled, scholarshipData[0].empty], 748 backgroundColor: ['#AFD1AA', '#B2D1DA'], 749 borderWidth: 1, 750 borderColor: '#F7F7F7', 751 cutout: '50%' 752 }, 753 { 754 data: [scholarshipData[1].filled, scholarshipData[1].empty], 755 backgroundColor: ['#31652B', '#B2D1DA'], 756 borderWidth: 1, 757 borderColor: '#F7F7F7', 758 cutout: '60%' 759 }, 760 ] 761 }, 762 options: { 763 responsive: true, 764 maintainAspectRatio: false, 765 plugins: { 766 legend: { display: false }, 767 tooltip: getTooltipConfig('Concessão de Bolsas') 768 } 769 } 770 }); 771 updateChartTotal('scholarshipChart', scholarshipTotal); 772 } 773 774 // Gráfico 7 - Aposentadorias, Reserva Remunerada e Reformas 775 const retirementChart3Ctx = document.getElementById('retirementChart3'); 776 if (retirementChart3Ctx) { 777 const retirementChart3Data = [ 778 { filled: 101, empty: 56 }, // Anel externo 779 { filled: 101, empty: 56 }, // Anel médio 780 ]; 781 const retirementChart3Total = retirementChart3Data.reduce((sum, ring) => sum + ring.filled, 0); 782 783 new Chart(retirementChart3Ctx, { 784 type: 'doughnut', 785 data: { 786 datasets: [ 787 { 788 data: [retirementChart3Data[0].filled, retirementChart3Data[0].empty], 789 backgroundColor: ['#AFD1AA', '#B2D1DA'], 790 borderWidth: 1, 791 borderColor: '#F7F7F7', 792 cutout: '50%' 793 }, 794 { 795 data: [retirementChart3Data[1].filled, retirementChart3Data[1].empty],
796 backgroundColor: ['#31652B', '#B2D1DA'], 797 borderWidth: 1, 798 borderColor: '#F7F7F7', 799 cutout: '60%' 800 }, 801 ] 802 }, 803 options: { 804 responsive: true, 805 maintainAspectRatio: false, 806 plugins: { 807 legend: { display: false }, 808 tooltip: getTooltipConfig('Aposentadorias e Reformas') 809 } 810 } 811 }); 812 updateChartTotal('retirementChart3', retirementChart3Total); 813 } 814 815 // Gráfico 8 - Aposentadorias, Reserva Remunerada e Reformas 816 const retirementChart4Ctx = document.getElementById('retirementChart4'); 817 if (retirementChart4Ctx) { 818 const retirementChart4Data = [ 819 { filled: 101, empty: 56 }, // Anel externo 820 { filled: 101, empty: 56 }, // Anel médio 821 ]; 822 const retirementChart4Total = retirementChart4Data.reduce((sum, ring) => sum + ring.filled, 0); 823 824 new Chart(retirementChart4Ctx, { 825 type: 'doughnut', 826 data: { 827 datasets: [ 828 { 829 data: [retirementChart4Data[0].filled, retirementChart4Data[0].empty], 830 backgroundColor: ['#AFD1AA', '#B2D1DA'], 831 borderWidth: 1, 832 borderColor: '#F7F7F7', 833 cutout: '50%' 834 }, 835 { 836 data: [retirementChart4Data[1].filled, retirementChart4Data[1].empty], 837 backgroundColor: ['#31652B', '#B2D1DA'], 838 borderWidth: 1, 839 borderColor: '#F7F7F7', 840 cutout: '60%' 841 }, 842 ] 843 }, 844 options: { 845 responsive: true, 846 maintainAspectRatio: false, 847 plugins: { 848 legend: { display: false }, 849 tooltip: getTooltipConfig('Aposentadorias e Reformas') 850 } 851 } 852 }); 853 updateChartTotal('retirementChart4', retirementChart4Total); 854 } 855 856 // Gráfico 9 - Vencimento e Vantagens Fixas - Pessoa civil 857 const salaryChart3Ctx = document.getElementById('salaryChart3'); 858 if (salaryChart3Ctx) { 859 const salaryChart3Data = [ 860 { filled: 101, empty: 56 }, // Anel externo 861 { filled: 101, empty: 56 }, // Anel médio 862 ]; 863 const salaryChart3Total = salaryChart3Data.reduce((sum, ring) => sum + ring.filled, 0); 864 865 new Chart(salaryChart3Ctx, { 866 type: 'doughnut', 867 data: { 868 datasets: [ 869 { 870 data: [salaryChart3Data[0].filled, salaryChart3Data[0].empty], 871 backgroundColor: ['#AFD1AA', '#B2D1DA'], 872 borderWidth: 1, 873 borderColor: '#F7F7F7', 874 cutout: '50%' 875 }, 876 { 877 data: [salaryChart3Data[1].filled, salaryChart3Data[1].empty], 878 backgroundColor: ['#31652B', '#B2D1DA'], 879 borderWidth: 1, 880 borderColor: '#F7F7F7', 881 cutout: '60%' 882 }, 883 ] 884 }, 885 options: { 886 responsive: true, 887 maintainAspectRatio: false, 888 plugins: { 889 legend: { display: false }, 890 tooltip: getTooltipConfig('Vencimentos e Vantagens') 891 } 892 } 893 }); 894 updateChartTotal('salaryChart3', salaryChart3Total); 895 } 896 897 // Gráfico 10 - Vencimento e Vantagens Fixas - Pessoa civil 898 const salaryChart4Ctx = document.getElementById('salaryChart4'); 899 if (salaryChart4Ctx) { 900 const salaryChart4Data = [ 901 { filled: 101, empty: 56 }, // Anel externo 902 { filled: 101, empty: 56 }, // Anel médio 903 ]; 904 const salaryChart4Total = salaryChart4Data.reduce((sum, ring) => sum + ring.filled, 0); 905 906 new Chart(salaryChart4Ctx, { 907 type: 'doughnut', 908 data: { 909 datasets: [ 910 { 911 data: [salaryChart4Data[0].filled, salaryChart4Data[0].empty], 912 backgroundColor: ['#AFD1AA', '#B2D1DA'], 913 borderWidth: 1, 914 borderColor: '#F7F7F7', 915 cutout: '50%' 916 }, 917 { 918 data: [salaryChart4Data[1].filled, salaryChart4Data[1].empty],
919 backgroundColor: ['#31652B', '#B2D1DA'], 920 borderWidth: 1, 921 borderColor: '#F7F7F7', 922 cutout: '60%' 923 }, 924 ] 925 }, 926 options: { 927 responsive: true, 928 maintainAspectRatio: false, 929 plugins: { 930 legend: { display: false }, 931 tooltip: getTooltipConfig('Vencimentos e Vantagens') 932 } 933 } 934 }); 935 updateChartTotal('salaryChart4', salaryChart4Total); 936 } 937 938 // Gráfico 11 - [A detalhar] 939 const detailChart2Ctx = document.getElementById('detailChart2'); 940 if (detailChart2Ctx) { 941 const detailChart2Data = [ 942 { filled: 101, empty: 56 }, // Anel externo 943 { filled: 101, empty: 56 }, // Anel médio 944 ]; 945 const detailChart2Total = detailChart2Data.reduce((sum, ring) => sum + ring.filled, 0); 946 947 new Chart(detailChart2Ctx, { 948 type: 'doughnut', 949 data: { 950 datasets: [ 951 { 952 data: [detailChart2Data[0].filled, detailChart2Data[0].empty], 953 backgroundColor: ['#AFD1AA', '#B2D1DA'], 954 borderWidth: 1, 955 borderColor: '#F7F7F7', 956 cutout: '50%' 957 }, 958 { 959 data: [detailChart2Data[1].filled, detailChart2Data[1].empty], 960 backgroundColor: ['#31652B', '#B2D1DA'], 961 borderWidth: 1, 962 borderColor: '#F7F7F7', 963 cutout: '60%' 964 }, 965 ] 966 }, 967 options: { 968 responsive: true, 969 maintainAspectRatio: false, 970 plugins: { 971 legend: { display: false }, 972 tooltip: getTooltipConfig('A Detalhar') 973 } 974 } 975 }); 976 updateChartTotal('detailChart2', detailChart2Total); 977 } 978 979 // Gráfico 12 - [A detalhar] 980 const detailChart3Ctx = document.getElementById('detailChart3'); 981 if (detailChart3Ctx) { 982 const detailChart3Data = [ 983 { filled: 101, empty: 56 }, // Anel externo 984 { filled: 101, empty: 56 }, // Anel médio 985 ]; 986 const detailChart3Total = detailChart3Data.reduce((sum, ring) => sum + ring.filled, 0); 987 988 new Chart(detailChart3Ctx, { 989 type: 'doughnut', 990 data: { 991 datasets: [ 992 { 993 data: [detailChart3Data[0].filled, detailChart3Data[0].empty], 994 backgroundColor: ['#AFD1AA', '#B2D1DA'], 995 borderWidth: 1, 996 borderColor: '#F7F7F7', 997 cutout: '50%' 998 }, 999 { 1000 data: [detailChart3Data[1].filled, detailChart3Data[1].empty], 1001 backgroundColor: ['#31652B', '#B2D1DA'], 1002 borderWidth: 1, 1003 borderColor: '#F7F7F7', 1004 cutout: '60%' 1005 }, 1006 ] 1007 }, 1008 options: { 1009 responsive: true, 1010 maintainAspectRatio: false, 1011 plugins: { 1012 legend: { display: false }, 1013 tooltip: getTooltipConfig('A Detalhar') 1014 } 1015 } 1016 }); 1017 updateChartTotal('detailChart3', detailChart3Total); 1018 } 1019} 1020 1021// Função para atualizar a legenda do gráfico de contratos 1022function updateContractsLegend(labels, counts, percentages) { 1023 const chartCard = document.getElementById('contractsChart').closest('.chart-card'); 1024 if (!chartCard) return; 1025 const legend = chartCard.querySelector('.chart-legend'); 1026 if (!legend) return; 1027 1028 const legendItems = Array.from(legend.querySelectorAll('.legend-item')); 1029 const count = Math.min(legendItems.length, labels.length, counts.length, percentages.length); 1030 1031 for (let i = 0; i < count; i++) { 1032 const item = legendItems[i]; 1033 const textEl = item.querySelector('.legend-text'); 1034 if (!textEl) continue; 1035 1036 const countValue = counts[i]; 1037 const percentage = percentages[i]; 1038 const label = labels[i]; 1039 1040 // Formatar percentual com 1 casa decimal 1041 const formattedPercentage = percentage.toFixed(1).replace('.', ','); 1042 1043 textEl.innerHTML = `<span class="legend-number">${formattedPercentage}%</span> ${label}`; 1044 } 1045} 1046 1047// Função para criar o gráfico de contratos 1048function createContractsChart() { 1049 const ctx = document.getElementById('contractsChart'); 1050 if (!ctx) return; 1051 1052 const dataUrl = '../land/public/data/contratos.json'; 1053 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 1054 1055 fetch(urlWithBust, { cache: 'no-store' }) 1056 .then(resp => { 1057 if (!resp.ok) throw new Error('fetch not ok'); 1058 return resp.json(); 1059 }) 1060 .then(json => { 1061 const labels = json.map(item => item.categoria); 1062 const counts = json.map(item => Number(item.count) || 0); 1063 const total = counts.reduce((sum, count) => sum + count, 0); 1064 1065 // Calcular percentuais 1066 const percentages = counts.map(count => (count / total) * 100); 1067 1068 const chartData = { 1069 labels: labels, 1070 datasets: [{ 1071 data: percentages, 1072 backgroundColor: [ 1073 '#AB2D2D', 1074 '#FB8585', 1075 '#31652B', 1076 '#67A95E', 1077 '#326879', 1078 '#8B4513' 1079 ], 1080 borderWidth: 0, 1081 cutout: '60%' 1082 }] 1083 }; 1084 1085 const config = { 1086 type: 'doughnut', 1087 data: chartData, 1088 options: { 1089 responsive: true, 1090 maintainAspectRatio: false, 1091 plugins: { 1092 legend: { display: false }, 1093 tooltip: { 1094 enabled: false, 1095 external: externalDoughnutTooltipHandler, 1096 backgroundColor: '#422278',
1097 titleColor: '#FFFFFF', 1098 bodyColor: '#FFFFFF', 1099 padding: 16, 1100 cornerRadius: 8, 1101 displayColors: true, 1102 borderColor: '#7A34F3', 1103 borderWidth: 1, 1104 titleFont: { 1105 size: 14, 1106 weight: '600', 1107 family: 'Inter' 1108 }, 1109 bodyFont: { 1110 size: 13, 1111 weight: '400', 1112 family: 'Inter' 1113 }, 1114 callbacks: { 1115 title: function(context) { 1116 return 'Contratos por Categoria'; 1117 }, 1118 label: function(context) { 1119 const index = context.dataIndex; 1120 const count = counts[index]; 1121 const percentage = context.parsed; 1122 return context.label + ': ' + count.toLocaleString('pt-BR') + ' contratos (' + percentage.toFixed(1).replace('.', ',') + '%)'; 1123 } 1124 } 1125 } 1126 }, 1127 elements: { arc: { borderWidth: 0 } } 1128 } 1129 }; 1130 1131 new Chart(ctx, config); 1132 1133 // Atualizar a legenda com os dados reais 1134 updateContractsLegend(labels, counts, percentages); 1135 }) 1136 .catch(() => { 1137 // Fallback com dados estáticos caso o JSON não carregue 1138 const chartData = { 1139 labels: [ 1140 'Serviços', 1141 'Compras', 1142 'Informática', 1143 'Mão de obra', 1144 'Serviços de Engenharia', 1145 'Cessão' 1146 ], 1147 datasets: [{ 1148 data: [59.54, 24.81, 8.78, 3.82, 2.67, 0.38], 1149 backgroundColor: [ 1150 '#AB2D2D', 1151 '#FB8585', 1152 '#31652B', 1153 '#67A95E', 1154 '#326879', 1155 '#8B4513' 1156 ], 1157 borderWidth: 0, 1158 cutout: '60%' 1159 }] 1160 }; 1161 1162 const config = { 1163 type: 'doughnut', 1164 data: chartData, 1165 options: { 1166 responsive: true, 1167 maintainAspectRatio: false, 1168 plugins: { 1169 legend: { display: false }, 1170 tooltip: { 1171 enabled: false, 1172 external: externalDoughnutTooltipHandler, 1173 backgroundColor: '#422278', 1174 titleColor: '#FFFFFF', 1175 bodyColor: '#FFFFFF', 1176 padding: 16, 1177 cornerRadius: 8, 1178 displayColors: true, 1179 borderColor: '#7A34F3', 1180 borderWidth: 1, 1181 titleFont: { 1182 size: 14, 1183 weight: '600', 1184 family: 'Inter' 1185 }, 1186 bodyFont: { 1187 size: 13, 1188 weight: '400', 1189 family: 'Inter' 1190 }, 1191 callbacks: { 1192 title: function(context) { 1193 return 'Contratos por Categoria'; 1194 }, 1195 label: function(context) { 1196 return context.label + ': ' + context.parsed.toFixed(2).replace('.', ',') + '%'; 1197 } 1198 } 1199 } 1200 }, 1201 elements: { arc: { borderWidth: 0 } } 1202 } 1203 }; 1204 1205 new Chart(ctx, config); 1206 }); 1207} 1208 1209// Carregar e desenhar os gráficos "Como o dinheiro está sendo gasto?" a partir do JSON 1210function loadExpenseElementCharts() { 1211 const dataUrl = '../land/public/data/orcamento_por_elemento_despesa.json'; 1212 const urlWithBust = `${dataUrl}?v=${Date.now()}`; 1213 1214 function safeNum(v) { return Number(v) || 0; } 1215 1216 // Mantém o esquema de cores original por gráfico 1217 function getRingColors(canvasId) { 1218 // [empenhado, pagas, aPagar] 1219 const purple = ['#AA79FE', '#7A34F3', '#422278']; 1220 const green = ['#AFD1AA', '#31652B', '#B2D1DA']; 1221 const warm = ['#AB2D2D', '#F19F42', '#E7D551']; 1222 switch (canvasId) { 1223 case 'retirementChart': 1224 case 'salaryChart': 1225 return purple; 1226 case 'detailChart': 1227 return warm; 1228 // Segunda, terceira e quarta linhas originais usavam verde
1228s 1229 case 'retirementChart2': 1230 case 'salaryChart2': 1231 case 'scholarshipChart': 1232 case 'retirementChart3': 1233 case 'retirementChart4': 1234 case 'salaryChart3': 1235 case 'salaryChart4': 1236 case 'detailChart2': 1237 case 'detailChart3': 1238 return green; 1239 default: 1240 return green; 1241 } 1242 } 1243 1244 function createElementDonut(canvasId, record) { 1245 const ctx = document.getElementById(canvasId); 1246 if (!ctx || !record) return; 1247 1248 // Base prioriza dotação; se ausente, usa empenhado; senão soma de pagas + a pagar 1249 const dotacao = safeNum(record.dotacao); 1250 const empenhado = safeNum(record.orcamento_alocado_empenhado); 1251 const pagas = safeNum(record.despesas_pagas); 1252 const aPagar = safeNum(record.despesas_programas_a_pagar); 1253 const base = dotacao > 0 ? dotacao : (empenhado > 0 ? empenhado : (pagas + aPagar)); 1254 if (base <= 0) return; 1255 1256 const valEmpenhado = Math.max(Math.min(empenhado, base), 0); 1257 const valPagas = Math.max(Math.min(pagas, base), 0); 1258 const valAPagar = Math.max(Math.min(aPagar, base), 0); 1259 1260 // destrói gráfico anterior se existir 1261 window.__govhubCharts = window.__govhubCharts || {}; 1262 if (window.__govhubCharts[canvasId]) { 1263 try { window.__govhubCharts[canvasId].destroy(); } catch (e) {} 1264 } 1265 1266 const [cEmp, cPag, cAPag] = getRingColors(canvasId); 1267 1268 const chartInstance = new Chart(ctx, { 1269 type: 'doughnut', 1270 data: { 1271 datasets: [ 1272 { label: 'orcamento_alocado_empenhado', data: [valEmpenhado, Math.max(base - valEmpenhado, 0)], backgroundColor: [cEmp, 'rgba(0,0,0,0)'], borderWidth: 1, borderColor: '#F7F7F7', cutout: '60%' }, 1273 { label: 'despesas_pagas', data: [valPagas, Math.max(base - valPagas, 0)], backgroundColor: [cPag, 'rgba(0,0,0,0)'], borderWidth: 1, borderColor: '#F7F7F7', cutout: '60%' }, 1274 { label: 'despesas_programas_a_pagar', data: [valAPagar, Math.max(base - valAPagar, 0)], backgroundColor: [cAPag, 'rgba(0,0,0,0)'], borderWidth: 1, borderColor: '#F7F7F7', cutout: '60%' } 1275 ] 1276 }, 1277 options: { 1278 responsive: true, 1279 maintainAspectRatio: false, 1280 plugins: { 1281 legend: { display: false }, 1282 tooltip: { 1283 enabled: false, 1284 external: externalDoughnutTooltipHandler, 1285 backgroundColor: '#422278', 1286 titleColor: '#FFFFFF', 1287 bodyColor: '#FFFFFF', 1288 padding: 12, 1289 borderColor: '#7A34F3', 1290 borderWidth: 1, 1291 displayColors: true, 1292 filter: function(item) { return item.dataIndex === 0; }, 1293 callbacks: { 1294 title: function(items) { 1295 if (!items || !items.length) return ''; 1296 const dsLabel = items[0].dataset.label || ''; 1297 const map = { 1298 'orcamento_alocado_empenhado': 'Orçamento alocado (empenhado)', 1299 'despesas_pagas': 'Despesas pagas', 1300 'despesas_programas_a_pagar': 'Despesas programadas (a pagar)' 1301 }; 1302 return map[dsLabel] || dsLabel; 1303 }, 1304 label: function(context) { 1305 const raw = context.parsed; 1306 const formatted = (typeof raw === 'number') ? raw.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : raw; 1307 return formatted; 1308 } 1309 } 1310 } 1311 } 1312 } 1313 }); 1314 1315 window.__govhubCharts[canvasId] = chartInstance; 1316 1317 // Atualiza o valor central como Dotação (ou base) em milhões 1318 const totalMillions = Math.round((dotacao > 0 ? dotacao : base) / 1_000_000); 1319 updateChartTotal(canvasId, totalMillions); 1320 1321 // Atualiza o tÃtulo do card para o nome do elemento de despesa do JSON 1322 const card = ctx.closest('.dashboard-card'); 1323 if (card) { 1324 const titleEl = card.querySelector('.dashboard-title'); 1325 if (titleEl && record.elemento_despesa_desc) { 1326 titleEl.textContent = record.elemento_despesa_desc; 1327 } 1328 } 1329 } 1330 1331 function indexByDesc(rows) { 1332 const map = new Map(); 1333 rows.forEach(r => map.set((r.elemento_despesa_desc || '').toUpperCase(), r)); 1334 return map; 1335 } 1336 1337 fetch(urlWithBust, { cache: 'no-store' }) 1338 .then(r => { if (!r.ok) throw new Error('fetch not ok'); return r.json(); }) 1339 .then(json => { 1340 let rows = Array.isArray(json) ? json.slice() : []; 1341 // Empurra "[A DETALHAR]" para o final 1342 rows.sort((a,b) => { 1343 const ad = (a.elemento_despesa_desc || '').toUpperCase() === '[A DETALHAR]'; 1344 const bd = (b.elemento_despesa_desc || '').toUpperCase() === '[A DETALHAR]'; 1345 if (ad && !bd) return 1; 1346 if (!ad && bd) return -1; 1347 return 0; 1348 }); 1349 const byDesc = indexByDesc(rows); 1350 1351 const mapping = [ 1352 { id: 'retirementChart', key: 'APOSENTADORIAS, RESERVA REMUNERADA E REFORMAS' }, 1353 { id: 'salaryChart', key: 'VENCIMENTOS E VANTAGENS FIXAS
1353- PESSOAL CIVIL' }, 1354 { id: 'detailChart', key: '[A DETALHAR]' }, 1355 { id: 'retirementChart2', key: 'LOCACAO DE MAO-DE-OBRA' }, 1356 { id: 'salaryChart2', key: 'OBRIGACOES PATRONAIS' }, 1357 { id: 'scholarshipChart', key: 'AUXILIO FINANCEIRO A PESQUISADORES' }, 1358 { id: 'retirementChart3', key: 'PENSOES' }, 1359 { id: 'retirementChart4', key: 'INDENIZACOES E RESTITUICOES' }, 1360 { id: 'salaryChart3', key: 'SERVICOS DE TECNOLOGIA DA INFORMACAO E COMUNICACAO - PJ' }, 1361 { id: 'salaryChart4', key: 'CONTRIBUICAO A ENTIDADE FECHADA PREVIDENCIA' }, 1362 { id: 'detailChart2', key: 'OUTROS SERVICOS DE TERCEIROS PJ - OP.INT.ORC.' }, 1363 { id: 'detailChart3', key: 'DIARIAS - PESSOAL CIVIL' } 1364 ]; 1365 1366 const usedKeys = new Set(); 1367 mapping.forEach(({ id, key }) => { 1368 const rec = byDesc.get(key); 1369 if (rec) { 1370 usedKeys.add(key); 1371 createElementDonut(id, rec); 1372 } 1373 }); 1374 1375 // Renderizar cartões adicionais para elementos não mapeados 1376 const remaining = rows.filter(r => !usedKeys.has((r.elemento_despesa_desc || '').toUpperCase())); 1377 if (remaining.length > 0) { 1378 const container = document.querySelector('.expenses-section .container'); 1379 if (container) { 1380 let rowDiv = null; 1381 remaining.forEach((rec, idx) => { 1382 if (idx % 3 === 0) { 1383 rowDiv = document.createElement('div'); 1384 rowDiv.className = 'row g-4 mb-4'; 1385 container.appendChild(rowDiv); 1386 } 1387 const col = document.createElement('div'); 1388 col.className = 'col-12 col-lg-4'; 1389 const card = document.createElement('div'); 1390 card.className = 'dashboard-card'; 1391 const title = document.createElement('h6'); 1392 title.className = 'dashboard-title'; 1393 title.textContent = rec.elemento_despesa_desc || ''; 1394 const chartContainer = document.createElement('div'); 1395 chartContainer.className = 'dashboard-chart-container'; 1396 const canvas = document.createElement('canvas'); 1397 const canvasId = `dynamicChart_${idx}`; 1398 canvas.id = canvasId; 1399 canvas.width = 200; 1400 canvas.height = 200; 1401 const center = document.createElement('div'); 1402 center.className = 'chart-center-text'; 1403 const num = document.createElement('span'); 1404 num.className = 'chart-total-number'; 1405 num.textContent = '0M'; 1406 const label = document.createElement('span'); 1407 label.className = 'chart-total-label'; 1408 label.textContent = 'Dotação'; 1409 center.appendChild(num); 1410 center.appendChild(label); 1411 chartContainer.appendChild(canvas); 1412 chartContainer.appendChild(center); 1413 card.appendChild(title); 1414 card.appendChild(chartContainer); 1415 col.appendChild(card); 1416 rowDiv.appendChild(col); 1417 createElementDonut(canvasId, rec); 1418 }); 1419 } 1420 } 1421 1422 // Mover todos os cards "[A DETALHAR]" para o final visualmente 1423 const container = document.querySelector('.expenses-section .container'); 1424 if (container) { 1425 const detailCols = Array.from(container.querySelectorAll('.dashboard-card .dashboard-title')) 1426 .filter(h => (h.textContent || '').trim().toUpperCase() === '[A DETALHAR]') 1427 .map(h => h.closest('.col-12.col-lg-4')); 1428 if (detailCols.length) { 1429 // cria uma nova linha ao final para agrupar os "A DETALHAR" 1430 const detailRow = document.createElement('div'); 1431 detailRow.className = 'row g-4 mb-4'; 1432 container.appendChild(detailRow);
1433 detailCols.forEach(col => { 1434 if (col && col.parentElement) { 1435 detailRow.appendChild(col); 1436 } 1437 }); 1438 } 1439 // Reflow: reconstroi as linhas em grupos de 3 para eliminar buracos visuais 1440 const allCols = Array.from(container.querySelectorAll('.col-12.col-lg-4')); 1441 // Remove todas as linhas atuais 1442 Array.from(container.querySelectorAll('.row.g-4.mb-4')).forEach(row => row.remove()); 1443 // Recria as linhas com 3 colunas por linha 1444 for (let i = 0; i < allCols.length; i += 3) { 1445 const row = document.createElement('div'); 1446 row.className = 'row g-4 mb-4'; 1447 container.appendChild(row); 1448 allCols.slice(i, i + 3).forEach(col => row.appendChild(col)); 1449 } 1450 } 1451 }) 1452 .catch(() => { 1453 // mantém gráficos estáticos se falhar 1454 }); 1455} 1456 1457// Função para inicializar funcionalidades especÃficas da página de dashboards 1458function initDashboards() { 1459 // Criar o gráfico quando o DOM estiver carregado 1460 if (document.readyState === 'loading') { 1461 document.addEventListener('DOMContentLoaded', () => { 1462 createBudgetChart(); 1463 createContractsChart(); 1464 createDashboardCharts(); 1465 }); 1466 } else { 1467 createBudgetChart(); 1468 createContractsChart(); 1469 createDashboardCharts(); 1470 } 1471 1472 console.log('ð Página Dashboards inicializada com sucesso!'); 1473} 1474 1475// ======================================== 1476// ECHARTS CHARTS - GENDER AND RACE DISTRIBUTION 1477// ======================================== 1478 1479// Função para criar o gráfico em formato V para distribuição por gênero 1480function createGenderVChart() { 1481 // Gráfico de gênero agora é HTML/CSS estático, não precisa de JavaScript 1482 return; 1483} 1484 1485// Função para atualizar o gráfico de gênero com dados do JSON 1486async function updateGenderChart() { 1487 try { 1488 // Carregar dados do JSON 1489 const response = await fetch('../land/public/data/servidores_sexo.json'); 1490 if (!response.ok) { 1491 throw new Error('Erro ao carregar dados de gênero'); 1492 } 1493 const data = await response.json(); 1494 1495 // Encontrar os dados de feminino e masculino 1496 let femininoPercent = 0; 1497 let masculinoPercent = 0; 1498 1499 data.forEach(item => { 1500 if (item.nome_sexo === 'FEMININO' && item.feminino !== null) { 1501 femininoPercent = item.feminino * 100; 1502 } else if (item.nome_sexo === 'MASCULINO' && item.masculino !== null) { 1503 masculinoPercent = item.masculino * 100; 1504 } 1505 }); 1506 1507 // Atualizar as barras do gráfico 1508 const femininoBar = document.querySelector('.bar-fill.feminino'); 1509 const masculinoBar = document.querySelector('.bar-fill.masculino'); 1510 const femininoValue = document.querySelector('.bar-item:first-child .bar-value'); 1511 const masculinoValue = document.querySelector('.bar-item:last-child .bar-value'); 1512 1513 if (femininoBar && femininoValue) { 1514 femininoBar.style.width = `${femininoPercent}%`; 1515 femininoValue.textContent = `${femininoPercent.toFixed(1)}%`; 1516 } 1517 1518 if (masculinoBar && masculinoValue) { 1519 masculinoBar.style.width = `${masculinoPercent}%`; 1520 masculinoValue.textContent = `${masculinoPercent.toFixed(1)}%`; 1521 } 1522 1523 console.log('Gráfico de gênero atualizado com sucesso'); 1524 1525 } catch (error) { 1526 console.error('Erro ao atualizar gráfico de gênero:', error); 1527 } 1528} 1529 1530// Função para atualizar o gráfico automaticamente a cada 30 segundos 1531function startGenderChartAutoUpdate() { 1532 // Atualizar imediatamente 1533 updateGenderChart(); 1534 1535 // Configurar atualização automática a cada 30 segundos 1536 setInterval(updateGenderChart, 30000); 1537 1538 console.log('Atualização automática do gráfico de gênero iniciada (30s)'); 1539} 1540 1541// Função para criar o treemap de distribuição por raça/cor 1542// Função para carregar dados de raça/cor do JSON 1543async function loadServidoresCorData() { 1544 try { 1545 const response = await fetch('../land/public/data/servidores_cor.json'); 1546 if (!response.ok) { 1547 throw new Error(`HTTP error! status: ${response.status}`); 1548 } 1549 const data = await response.json(); 1550 console.log('â Dados de servidores por cor carregados:', data); 1551 return data; 1552 } catch (error) { 1553 console.error('â Erro ao carregar dados de servidores por cor:', error); 1554 return null; 1555 } 1556} 1557 1558// Função para mapear dados do JSON para o formato do gráfico 1559function mapServidoresCorToChartData(jsonData) { 1560 if (!jsonData || !Array.isArray(jsonData)) { 1561 console.error('â Dados inválidos para mapeamento'); 1562 return []; 1563 } 1564 1565 // Mapeamento de cores para cada raça/cor 1566 const colorMap = { 1567 'BRANCA': '#FF8C00', 1568 'PARDA': '#FFA500', 1569 'PRETA': '#8A2BE2', 1570 'AMARELA': '#9370DB',
1571 'NAO INFORMADO': '#DDA0DD', 1572 'INDIGENA': '#9932CC', 1573 '': '#DDA0DD' // Para valores vazios 1574 }; 1575 1576 // Filtrar dados válidos e mapear para o formato do gráfico 1577 const chartData = jsonData 1578 .filter(item => item.nome_cor && item.cor_ou_raca > 0) // Filtrar apenas dados válidos 1579 .map(item => ({ 1580 name: item.nome_cor, 1581 value: item.cor_ou_raca, 1582 itemStyle: { 1583 color: colorMap[item.nome_cor] || '#DDA0DD', 1584 borderColor: '#ffffff', 1585 borderWidth: 2 1586 } 1587 })) 1588 .sort((a, b) => b.value - a.value); // Ordenar por valor decrescente 1589 1590 console.log('ð Dados mapeados para o gráfico:', chartData); 1591 return chartData; 1592} 1593 1594function createRaceTreemap() { 1595 const chartDom = document.getElementById('raceChart'); 1596 if (!chartDom) return; 1597 1598 const myChart = echarts.init(chartDom); 1599 1600 // Carregar dados do JSON e criar o gráfico 1601 loadServidoresCorData().then(jsonData => { 1602 const data = mapServidoresCorToChartData(jsonData); 1603 1604 if (data.length === 0) { 1605 console.error('â Nenhum dado válido encontrado para o gráfico'); 1606 return; 1607 } 1608 1609 const option = { 1610 backgroundColor: 'transparent', 1611 tooltip: { 1612 trigger: 'item', 1613 backgroundColor: '#422278', 1614 borderColor: '#7A34F3', 1615 borderWidth: 1, 1616 textStyle: { 1617 color: '#ffffff', 1618 fontFamily: 'Inter' 1619 }, 1620 formatter: function(params) { 1621 return params.name + '<br/>' + params.value + ' funcionários'; 1622 } 1623 }, 1624 series: [ 1625 { 1626 name: 'Distribuição por Raça/Cor', 1627 type: 'treemap', 1628 data: data, 1629 roam: false, 1630 nodeClick: false, 1631 breadcrumb: { 1632 show: false 1633 }, 1634 label: { 1635 show: true, 1636 formatter: function(params) { 1637 // Definir limite de valor para mostrar rótulo dentro do treemap 1638 // Itens com valor abaixo deste limite terão o rótulo na legenda externa 1639 const minLabelValue = 28; // Baseado nos dados: 28 PRETA aparece, 1 e 2 não 1640 1641 // Mostrar rótulo dentro do treemap apenas se o valor for maior ou igual ao limite 1642 if (params.value >= minLabelValue) { 1643 return params.value + '\n' + params.name; 1644 } else { 1645 return ''; // Ocultar rótulo para itens pequenos (aparecerão na legenda) 1646 } 1647 }, 1648 fontSize: function(params) { 1649 // Tamanho de fonte baseado no valor 1650 if (params.value >= 500) return 16; 1651 if (params.value >= 100) return 14; 1652 if (params.value >= 50) return 12; 1653 return 10; 1654 }, 1655 fontWeight: 'bold', 1656 color: '#ffffff', 1657 fontFamily: 'Inter', 1658 textShadowColor: 'rgba(0,0,0,0.5)', 1659 textShadowBlur: 2, 1660 position: 'inside', 1661 align: 'center', 1662 verticalAlign: 'middle' 1663 }, 1664 upperLabel: { 1665 show: false 1666 }, 1667 itemStyle: { 1668 borderColor: '#ffffff', 1669 borderWidth: 1, 1670 borderRadius: 0 1671 }, 1672 emphasis: { 1673 itemStyle: { 1674 borderColor: '#7A34F3', 1675 borderWidth: 2 1676 } 1677 }, 1678 gapWidth: 0, 1679 gapHeight: 0, 1680 levels: [ 1681 { 1682 itemStyle: { 1683 borderColor: '#ffffff', 1684 borderWidth: 1, 1685 gapWidth: 0, 1686 gapHeight: 0 1687 } 1688 } 1689 ] 1690 } 1691 ] 1692 }; 1693 1694 myChart.setOption(option); 1695 1696 // Definir limite de valor para mostrar na legenda (mesmo valor usado no formatter) 1697 const minLabelValue = 28; 1698 1699 // Limpar legenda existente se houver 1700 const chartContainer = document.getElementById('raceChart').parentElement; 1701 const existingLegend = chartContainer.querySelector('.race-chart-legend'); 1702 if (existingLegend) { 1703 existingLegend.remove(); 1704 } 1705 1706 // Criar novo contêiner da legenda 1707 const legendContainer = document.createElement('div'); 1708 legendContainer.className = 'race-chart-legend'; 1709 legendContainer.style.cssText = `
1710 display: flex; 1711 flex-direction: row; 1712 gap: 20px; 1713 margin-top: 10px; 1714 font-family: Inter, sans-serif; 1715 width: 100%; 1716 max-width: 100%; 1717 overflow-x: auto; 1718 overflow-y: hidden; 1719 box-sizing: border-box; 1720 padding-bottom: 8px; 1721 scrollbar-width: thin; 1722 scrollbar-color: #7A34F3 #f1f1f1; 1723 `; 1724 1725 // Gerar itens da legenda para categorias abaixo do limite 1726 const legendItems = data.filter(item => item.value < minLabelValue); 1727 1728 legendItems.forEach(item => { 1729 const legendItem = document.createElement('div'); 1730 legendItem.style.cssText = ` 1731 display: flex; 1732 align-items: center; 1733 gap: 8px; 1734 font-size: 14px; 1735 color: #1f2937; 1736 flex-shrink: 0; 1737 white-space: nowrap; 1738 min-width: fit-content; 1739 box-sizing: border-box; 1740 `; 1741 1742 const legendDot = document.createElement('div'); 1743 legendDot.style.cssText = ` 1744 width: 12px; 1745 height: 12px; 1746 background-color: ${item.itemStyle.color}; 1747 border-radius: 50%; 1748 `; 1749 1750 legendItem.appendChild(legendDot); 1751 1752 // Formatar nome para a legenda (com acentos e capitalização) 1753 let displayName = item.name; 1754 if (item.name === 'NAO INFORMADO') { 1755 displayName = 'Não informados'; 1756 } else if (item.name === 'INDIGENA') { 1757 displayName = 'IndÃgenas'; 1758 } else if (item.name === 'AMARELA') { 1759 displayName = 'Amarelos'; 1760 } else if (item.name === 'PRETA') { 1761 displayName = 'Pretos'; 1762 } else if (item.name === 'PARDA') { 1763 displayName = 'Pardos'; 1764 } else if (item.name === 'BRANCA') { 1765 displayName = 'Brancos'; 1766 } 1767 1768 legendItem.appendChild(document.createTextNode(`${item.value} ${displayName}`)); 1769 legendContainer.appendChild(legendItem); 1770 }); 1771 1772 // Adicionar a legenda ao container do gráfico apenas se houver itens 1773 if (legendItems.length > 0) { 1774 chartContainer.appendChild(legendContainer); 1775 } 1776 1777 // Responsive com ajuste para mobile e desktop 1778 window.addEventListener('resize', function() { 1779 const chartDom = document.getElementById('raceChart'); 1780 const isMobile = window.innerWidth <= 768; 1781 const isSmallMobile = window.innerWidth <= 576; 1782 1783 if (chartDom) { 1784 if (isMobile) { 1785 // Ajustar altura no mobile 1786 chartDom.style.height = isSmallMobile ? '250px' : '300px'; 1787 } else { 1788 // Voltar para altura desktop 1789 chartDom.style.height = '400px'; 1790 } 1791 } 1792 1793 // Forçar atualização da legenda 1794 const legendContainer = chartContainer.querySelector('.race-chart-legend'); 1795 if (legendContainer) { 1796 legendContainer.style.display = 'flex'; 1797 legendContainer.style.visibility = 'visible'; 1798 legendContainer.style.opacity = '1'; 1799 } 1800 1801 myChart.resize(); 1802 }); 1803 1804 // Ajuste inicial baseado no tamanho da tela 1805 const chartDom = document.getElementById('raceChart'); 1806 const isMobile = window.innerWidth <= 768; 1807 const isSmallMobile = window.innerWidth <= 576; 1808 1809 if (chartDom) { 1810 if (isMobile) { 1811 chartDom.style.height = isSmallMobile ? '250px' : '300px'; 1812 } else { 1813 chartDom.style.height = '400px'; 1814 } 1815 myChart.resize(); 1816 } 1817 1818 // Garantir que a legenda seja visÃvel 1819 setTimeout(() => { 1820 const legendContainer = chartContainer.querySelector('.race-chart-legend'); 1821 if (legendContainer) { 1822 legendContainer.style.display = 'flex'; 1823 legendContainer.style.visibility = 'visible'; 1824 legendContainer.style.opacity = '1'; 1825 } 1826 }, 100); 1827 1828 }).catch(error => { 1829 console.error('â Erro ao criar gráfico de raça/cor:', error); 1830 }); 1831} 1832 1833// Função para inicializar funcionalidades especÃficas da página de dashboards 1834function initDashboards() { 1835 // Criar o gráfico quando o DOM estiver carregado 1836 if (document.readyState === 'loading') { 1837 document.addEventListener('DOMContentLoaded', () => { 1838 createBudgetChart(); 1839 createContractsChart(); 1840 // createDashboardCharts(); // removido para evitar gráficos duplicados 1841 loadExpenseElementCharts(); 1842 loadBudgetKpis(); 1843 loadContractsKpis(); 1844 loadContractsBars();
1845 loadTedsRecebidosKpis(); 1846 createGenderVChart(); 1847 startGenderChartAutoUpdate(); 1848 createRaceTreemap(); 1849 loadTedsRecebidosTable(); 1850 loadTedsEnviadosTable(); 1851 loadTedsEnviadosKpis(); 1852 loadEmployeeData(); // Carregar dados de funcionários 1853 createAposentadoriasChart(); 1854 }); 1855 } else { 1856 createBudgetChart(); 1857 createContractsChart(); 1858 // createDashboardCharts(); // removido para evitar gráficos duplicados 1859 loadExpenseElementCharts(); 1860 loadBudgetKpis(); 1861 loadContractsKpis(); 1862 loadContractsBars(); 1863 loadTedsRecebidosKpis(); 1864 createGenderVChart(); 1865 startGenderChartAutoUpdate(); 1866 createRaceTreemap(); 1867 loadTedsRecebidosTable(); 1868 loadTedsEnviadosTable(); 1869 loadTedsEnviadosKpis(); 1870 loadEmployeeData(); // Carregar dados de funcionários 1871 createAposentadoriasChart(); 1872 } 1873 1874 console.log('ð Página Dashboards inicializada com sucesso!'); 1875} 1876 1877// Função para carregar e popular a tabela de TEDs recebidos 1878function loadTedsRecebidosTable() {
1879 fetch('../land/public/data/detalhamento_teds_recebidos.json') 1880 .then(resp => resp.json()) 1881 .then(json => { 1882 const tbody = document.getElementById('teds-recebidos-tbody'); 1883 if (!tbody) return; 1884 1885 // Limpar tbody 1886 tbody.innerHTML = ''; 1887 1888 // Para cada item do JSON, criar uma linha 1889 json.forEach(item => { 1890 const tr = document.createElement('tr'); 1891 1892 // Formatar valor monetário 1893 const valorFormatado = (item.valor_firmado || 0).toLocaleString('pt-BR', { 1894 minimumFractionDigits: 2, 1895 maximumFractionDigits: 2 1896 }); 1897 1898 // Calcular percentuais 1899 const percentualTemporal = ((item.percentual_conclusao || 0) * 100).toFixed(2); 1900 const percentualOrcamentario = item.percentual_conclusao_orcamentaria 1901 ? ((item.percentual_conclusao_orcamentaria || 0) * 100).toFixed(2) 1902 : '0'; 1903 1904 tr.innerHTML = ` 1905 <td> 1906 <div class="teds-program"> 1907 <div class="teds-program-code">${item.unidade || ''}</div> 1908 <div class="teds-program-text">${item.programa || ''}</div> 1909 </div> 1910 </td> 1911 <td>${item.vigencia || ''}</td> 1912 <td>${valorFormatado}</td> 1913 <td> 1914 <div class="teds-progress"> 1915 <div class="teds-progress-bar" style="width: ${percentualTemporal}%"> 1916 <span class="teds-progress-handle" style="left: ${percentualOrcamentario}%"></span> 1917 </div> 1918 </div> 1919 </td> 1920 `; 1921 1922 tbody.appendChild(tr); 1923 }); 1924 }); 1925} 1926 1927// Função para carregar e popular a tabela de TEDs enviados 1928function loadTedsEnviadosTable() { 1929 fetch('../land/public/data/detalhamento_teds_enviados.json') 1930 .then(resp => resp.json()) 1931 .then(json => { 1932 const tbody = document.getElementById('teds-enviados-tbody'); 1933 if (!tbody) return; 1934 1935 // Limpar tbody 1936 tbody.innerHTML = ''; 1937 1938 // Para cada item do JSON, criar uma linha 1939 json.forEach(item => { 1940 const tr = document.createElement('tr'); 1941 1942 // Formatar valor monetário 1943 const valorFormatado = (item.valor_firmado || 0).toLocaleString('pt-BR', { 1944 minimumFractionDigits: 2, 1945 maximumFractionDigits: 2 1946 }); 1947 1948 // Calcular percentuais 1949 const percentualTemporal = ((item.percentual_conclusao || 0) * 100).toFixed(2); 1950 const percentualOrcamentario = item.percentual_conclusao_orcamentaria 1951 ? ((item.percentual_conclusao_orcamentaria || 0) * 100).toFixed(2) 1952 : '0'; 1953 1954 tr.innerHTML = ` 1955 <td> 1956 <div class="teds-sent-program"> 1957 <div class="teds-sent-program-code">${item.unidade_descentralizadora_responsavel || ''}</div> 1958 <div class="teds-sent-program-text">${item.programa || ''}</div> 1959 </div> 1960 </td> 1961 <td>${item.vigencia || ''}</td> 1962 <td>${valorFormatado}</td> 1963 <td> 1964 <div class="teds-sent-progress"> 1965 <div class="teds-sent-progress-bar" style="width: ${percentualTemporal}%"> 1966 <span class="teds-progress-handle" style="left: ${percentualOrcamentario}%"></span> 1967 </div> 1968 </div> 1969 </td> 1970 `; 1971 1972 tbody.appendChild(tr); 1973 }); 1974 }); 1975} 1976 1977// Função para carregar dados de servidores do JSON 1978async function loadServidoresData() { 1979 try { 1980 const response = await fetch('../land/public/data/servidores.json'); 1981 if (!response.ok) { 1982 throw new Error(`HTTP error! status: ${response.status}`); 1983 } 1984 const data = await response.json(); 1985 console.log('â Dados de servidores carregados:', data); 1986 return data; 1987 } catch (error) { 1988 console.error('â Erro ao carregar dados de servidores:', error); 1989 return null; 1990 } 1991} 1992 1993// Função para carregar e atualizar todos os dados de funcionários 1994async function loadEmployeeData() { 1995 try { 1996 const servidoresData = await loadServidoresData(); 1997 1998 if (servidoresData) { 1999 // Atualizar cards de funcionários ativos 2000 updateEmployeeCards(servidoresData); 2001 2002 // Atualizar card de aposentadorias 2003 updateTotalAposentadorias(servidoresData); 2004 2005 console.log('â Todos os dados de funcionários atualizados com sucesso!'); 2006 } 2007 } catch (error) { 2008 console.error('â Erro ao carregar dados de funcionários:', error); 2009 } 2010} 2011 2012// Função para atualizar o card de total de aposentadorias 2013function updateTotalAposentadorias(servidoresData) { 2014 if (!servidoresData || servidoresData.length === 0) { 2015 console.error('â Dados de servidores inválidos'); 2016 return; 2017 } 2018 2019 const totalAposentadorias = servidoresData[0].aposentados; 2020 const totalElement = document.getElementById('total-aposentadorias'); 2021 2022 if (totalElement) { 2023 totalElement.textContent = totalAposentadorias.toLocaleStr
2023ing('pt-BR'); 2024 } 2025} 2026 2027// Função para atualizar os cards de funcionários ativos 2028function updateEmployeeCards(servidoresData) { 2029 if (!servidoresData || servidoresData.length === 0) { 2030 console.error('â Dados de servidores inválidos'); 2031 return; 2032 } 2033 2034 const data = servidoresData[0]; 2035 2036 // Atualizar servidores ativos 2037 const servidoresElement = document.getElementById('servidores-ativos'); 2038 if (servidoresElement) { 2039 servidoresElement.textContent = data.servidores_ativos.toLocaleString('pt-BR'); 2040 } 2041 2042 // Atualizar estagiários 2043 const estagiariosElement = document.getElementById('estagiarios'); 2044 if (estagiariosElement) { 2045 estagiariosElement.textContent = data.estagiarios.toLocaleString('pt-BR'); 2046 } 2047 2048 // Atualizar terceirizados 2049 const terceirizadosElement = document.getElementById('terceirizados'); 2050 if (terceirizadosElement) { 2051 if (data.terceirizados === 0) { 2052 terceirizadosElement.textContent = '-'; 2053 } else { 2054 terceirizadosElement.textContent = data.terceirizados.toLocaleString('pt-BR'); 2055 } 2056 } 2057} 2058 2059// Função para gerar dados do gráfico baseados nos dados reais 2060const MOCK_APOSENTADORIAS_SERIE = [15, 15, 14, 13, 14, 12, 14, 15, 12, 12, 13, 14]; 2061 2062function generateAposentadoriasChartData() { 2063 return [...MOCK_APOSENTADORIAS_SERIE]; 2064} 2065 2066// Função para criar o gráfico de linha de aposentadorias 2067async function createAposentadoriasChart() { 2068 const ctx = document.getElementById('aposentadoriasChart'); 2069 if (!ctx || typeof Chart === 'undefined') { 2070 return; 2071 } 2072 2073 // Destruir gráfico existente se houver 2074 destroyChartIfExists('aposentadoriasChart'); 2075 2076 // Carregar dados de servidores 2077 const servidoresData = await loadServidoresData(); 2078 2079 // Atualizar o card de total de aposentadorias 2080 updateTotalAposentadorias(servidoresData); 2081 2082 // Atualizar os cards de funcionários ativos 2083 updateEmployeeCards(servidoresData); 2084 2085 // Gerar dados do gráfico baseados nos dados reais 2086 const chartData = generateAposentadoriasChartData(servidoresData); 2087 2088 const data = { 2089 labels: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'], 2090 datasets: [{ 2091 label: 'Aposentadorias por Mês', 2092 data: chartData, 2093 borderColor: '#66308F', // Cor roxa baseada em exemplo externo 2094 backgroundColor: '#66308F', 2095 borderWidth: 3, 2096 fill: false, 2097 tension: 0, // Linha mais angular, menos arredondada 2098 pointRadius: 0, // Remove os pontos 2099 pointHoverRadius: 0, // Remove os pontos no hover
2100 pointBackgroundColor: 'transparent', 2101 pointBorderColor: 'transparent', 2102 pointBorderWidth: 0 2103 }] 2104 }; 2105 2106 // Configuração baseada em exemplo externo do Chart.js 2107 const config = { 2108 type: 'line', 2109 data: data, 2110 options: { 2111 responsive: true, 2112 maintainAspectRatio: false, 2113 interaction: { 2114 intersect: false, 2115 mode: 'index' 2116 }, 2117 plugins: { 2118 legend: { 2119 display: false 2120 }, 2121 tooltip: { 2122 enabled: true, 2123 backgroundColor: '#66308F', 2124 titleColor: '#ffffff', 2125 bodyColor: '#ffffff', 2126 borderColor: 'transparent', 2127 borderWidth: 0, 2128 cornerRadius: 8, 2129 displayColors: false, 2130 padding: 8, 2131 titleFont: { 2132 family: 'Inter', 2133 size: 0 2134 }, 2135 bodyFont: { 2136 family: 'Inter', 2137 size: 14, 2138 weight: 'bold' 2139 }, 2140 callbacks: { 2141 title: function() { 2142 return ''; 2143 }, 2144 label: function(context) { 2145 return context.parsed.y.toString(); 2146 } 2147 }, 2148 // Posicionamento para aparecer abaixo do ponto 2149 position: 'nearest', 2150 xAlign: 'center', 2151 yAlign: 'bottom', 2152 caretSize: 8, 2153 caretPadding: 4 2154 } 2155 }, 2156 scales: { 2157 x: { 2158 display: true, 2159 grid: { 2160 color: '#e5e7eb', 2161 drawBorder: false, 2162 lineWidth: 1 2163 }, 2164 ticks: { 2165 color: '#6b7280', 2166 font: { 2167 family: 'Inter', 2168 size: 12, 2169 weight: '500' 2170 }, 2171 padding: 8 2172 } 2173 }, 2174 y: { 2175 display: true, 2176 beginAtZero: true, 2177 max: Math.max(...chartData) + 10, // Ajustar máximo baseado nos dados reais 2178 grid: { 2179 color: '#e5e7eb', 2180 drawBorder: false, 2181 lineWidth: 1 2182 }, 2183 ticks: { 2184 color: '#6b7280', 2185 font: { 2186 family: 'Inter', 2187 size: 12, 2188 weight: '500' 2189 }, 2190 stepSize: Math.ceil((Math.max(...chartData) + 10) / 10), 2191 padding: 8 2192 } 2193 } 2194 }, 2195 elements: { 2196 line: { 2197 borderJoinStyle: 'miter', // Junções mais angulares 2198 borderCapStyle: 'butt' // Pontas mais retas 2199 }, 2200 point: { 2201 hoverBackgroundColor: 'transparent', 2202 hoverBorderColor: 'transparent', 2203 hoverBorderWidth: 0 2204 } 2205 } 2206 } 2207 }; 2208 2209 const chartInstance = new Chart(ctx, config); 2210 // Armazenar referência do gráfico no canvas para facilitar destruição futura 2211 ctx.chart = chartInstance; 2212} 2213 2214// Função de fallback para garantir que o gráfico seja criado 2215function ensureAposentadoriasChart() { 2216 setTimeout(() => { 2217 const canvas = document.getElementById('aposentadoriasChart'); 2218 if (canvas && !canvas.chart) { 2219 console.log('ð Tentando criar gráfico novamente...'); 2220 createAposentadoriasChart(); 2221 } 2222 }, 1000); 2223} 2224 2225// Inicializar quando o script for carregado 2226initDashboards(); 2227 2228// Garantir que o gráfico seja criado 2229ensureAposentadoriasChart(); 2230 2231// Exportar funções para uso global 2232window.createBudgetChart = createBudgetChart; 2233window.createContractsChart = createContractsChart; 2234window.createDashboardCharts = createDashboardCharts; 2235window.createGenderVChart = createGenderVChart; 2236window.updateGenderChart = updateGenderChart; 2237window.startGenderChartAutoUpdate = startGenderChartAutoUpdate; 2238window.createRaceTreemap = createRaceTreemap; 2239window.createAposentadoriasChart = createAposentadoriasChart;
2240window.loadTedsRecebidosTable = loadTedsRecebidosTable; 2241window.loadTedsEnviadosTable = loadTedsEnviadosTable; 2242window.loadEmployeeData = loadEmployeeData; 2243window.initDashboards = initDashboards; 2244
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.