PageSourceSearch

https://gov-hub.io/govhub/dashboards/dashboards.js

js gov-hub.io collected 2026-10-08 03:25:16 UTC 87,095 bytes, 2,244 lines download raw bytes

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.