let dadosOriginais = [];
let chart = null;
let dadosFiltrados = [];
let perdas = null;
let bool_sla = false;
alert('teste');
function calcularTempoPermanencia(entrada, saida) {
const agora = new Date();
//console.log(saida);
// usa a saida informada ou agora
saida = (saida == '' || saida == null) ? agora : saida;
//console.log(saida);
if (!(entrada instanceof Date) || isNaN(entrada)) return 0;
if (!(saida instanceof Date) || isNaN(saida)) return 0;
const diffMs = saida - entrada;
const diffHoras = diffMs / (1000 * 60 * 60);
return Math.max(diffHoras, 0);
}
function date_f_iso_8601() {
const agora = new Date();
const ano = agora.getFullYear();
const mes = String(agora.getMonth() + 1).padStart(2, '0'); // Meses são baseados em zero
const dia = String(agora.getDate()).padStart(2, '0');
const horas = String(agora.getHours()).padStart(2, '0');
const minutos = String(agora.getMinutes()).padStart(2, '0');
const segundos = String(agora.getSeconds()).padStart(2, '0');
const dataFormatada = `${ano}-${mes}-${dia}T${horas}:${minutos}:${segundos}`;
return dataFormatada
}
function splitTextVertical(text) {
return text.split('').join('\n');
}
function renderizarGrafico(dados, sla) {
const agrupados = {};
const agora = new Date();
const target = 5;
dados.forEach(item => {
const prog = new Date(item.programacao_dt);
// usar SEMPRE data_entrada / data_saida
const entradaRaw = new Date(item.data_entrada);
const saida = item.data_saida != '' ? new Date(item.data_saida) : new Date(agora);
if (isNaN(prog) || isNaN(entradaRaw) || isNaN(saida)) return;
// aplicar SLA (se pedido)
const entradaCorrigida = sla && entradaRaw < prog ? prog : entradaRaw;
const dataFormatada = saida.toLocaleDateString('pt-BR');
const horas = sla
? (saida >= prog ? calcularTempoPermanencia(entradaCorrigida, saida) : 0)
: calcularTempoPermanencia(entradaCorrigida, saida);
const aderente = item.aderencia === true || String(item.aderencia).toLowerCase() === 'true';
if (!agrupados[dataFormatada]) {
agrupados[dataFormatada] = { totalHoras: 0, quantidade: 0, aderentes: 0 };
}
if (Number.isFinite(horas)) agrupados[dataFormatada].totalHoras += horas;
agrupados[dataFormatada].quantidade += 1;
if (aderente) agrupados[dataFormatada].aderentes += 1;
});
// Agora ordenamos as datas
const categorias = Object.keys(agrupados).sort((a, b) => {
const [diaA, mesA, anoA] = a.split('/').map(Number);
const [diaB, mesB, anoB] = b.split('/').map(Number);
const dateA = new Date(anoA, mesA - 1, diaA);
const dateB = new Date(anoB, mesB - 1, diaB);
return dateA - dateB;
});
const valores = categorias.map(data => {
const { totalHoras, quantidade } = agrupados[data];
const media = totalHoras / quantidade;
return media;
});
const aderencias = categorias.map(data => {
const { aderentes, quantidade } = agrupados[data];
return quantidade > 0 ? parseFloat(((aderentes / quantidade) * 100).toFixed(2)) : 0;
});
const options = {
chart: {
/*type: 'bar',*/
height: 400,
width: '100%',
toolbar: { show: false }
},
plotOptions: {
bar: {
columnWidth: '40%',
borderRadius: 4,
dataLabels: {
position: 'bottom',
orientation: 'vertical'
}
}
},
title: {
text: '',
align: 'center',
style: {
fontSize: '20px',
color: '#003369'
}
},
series: [
{
name: 'Média de Permanência',
type: 'bar',
data: valores
},
{
name: '% Aderência',
type: 'line',
data: aderencias,
yAxisIndex: 1
}
],
xaxis: {
categories: categorias,
labels: {
rotate: -45,
}
},
yaxis: [
{
min: 0,
max: 12,
tickAmount: 6,
labels: {
rotateAlways: true,
formatter: function (val) {
const horas = Math.floor(val);
const minutos = Math.round((val - horas) * 60);
return `${horas}h ${minutos}m`;
}
}
},
{
opposite: true,
title: { text: '% Aderência' },
min: 0,
max: 100,
tickAmount: 5,
labels: {
formatter: val => `${val.toFixed(0)}%`
}
}
],
grid: {
borderColor: '#e0e0e0',
strokeDashArray: 4,
},
colors: ['#003369', '#bfdbff'],
tooltip: {
y: {
formatter: function (val) {
const horas = Math.floor(val);
const minutos = Math.round((val - horas) * 60);
return `${horas}h ${minutos}m`;
}
}
},
dataLabels: {
enabled: true,
enabledOnSeries: [0, 1],
formatter: function (val, { seriesIndex }) {
if (seriesIndex === 0) {
// Série da média de permanência (em horas)
const h = Math.floor(val);
const m = Math.round((val - h) * 60);
return `${h}h ${m}m`;
} else {
// Série da aderência (em porcentagem)
return `${val.toFixed(0)}%`;
}
},
style: {
colors: ['#0072ca'], // cor do texto (branco em cima da barra azul)
fontSize: '12pt',
fontWeight: 'bold',
},
background: {
enabled: true // para não ter caixinha cinza atrás
}
},
tooltip: {
y: [
{
formatter: val => {
const h = Math.floor(val);
const m = Math.round((val - h) * 60);
return `${h}h ${m}m`;
}
},
{
formatter: val => `${val.toFixed(0)}%`
}
]
},
annotations: {
yaxis: [
{
y: 5,
opacity: 1,
strokeDashArray: 3,
borderColor: '#ffc238',
label: {
borderColor: '#ffc238',
textAnchor: 'end',
style: {
color: '#333',
background: '#ffc238',
padding: {
left: 5,
right: 5,
top: 0,
bottom: 2,
}
},
text: 'Target TAT'
}
}
]
}
};
if (chart) {
chart.destroy();
}
chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
}
function montarPerdas(dadosFiltrados, agora) {
perdas = dadosFiltrados.filter(item => item.aderencia === false);
perdas = perdas.map(item => {
const entrada = new Date(item.data_entrada);
const saida = item.data_saida ? new Date(item.data_saida) : new Date(agora);
const diffHoras = (saida - entrada) / (1000 * 60 * 60); // ms → horas
return {
...item,
permanencia: formatarHorasMinutos(diffHoras)
}
});
//console.log(perdas);
}
//Aplica filtros baseado no flag sla
function aplicarFiltros(sla) {
//const filtroParqueado = document.getElementById('filtro-parqueado')?.value || 'todos';
const filtroParqueado = document.querySelector('input[name="filtro-parqueado"]:checked')?.value || 'todos';
const filtroDataInicio = document.getElementById('filtro-data-inicio')?.value;
const filtroDataFim = document.getElementById('filtro-data-fim')?.value;
dadosFiltrados = [...dadosOriginais];
//console.log(dadosFiltrados);
const target = 5; // horas
const agora = date_f_iso_8601();
if (filtroParqueado !== 'todos') {
const parqueadoBool = filtroParqueado === 'true';
dadosFiltrados = dadosFiltrados.filter(item => item.parqueado === parqueadoBool);
}
if (filtroDataInicio && filtroDataFim) {
const dataInicio = new Date(`${filtroDataInicio}T00:00:00`);
const dataFim = new Date(`${filtroDataFim}T23:59:59`);
dadosFiltrados = dadosFiltrados.filter(item => {
const entrada = new Date(item.data_saida ? item.data_saida : agora);
return entrada >= dataInicio && entrada <= dataFim;
});
}
//Adiciona a propriedade "aderencia" a cada item filtrado, considerando flag sla
dadosFiltrados = dadosFiltrados.map(item => {
var entrada = new Date(item.data_entrada); const prog = new Date(item.programacao_dt);
if(sla) entrada = entrada < prog ? prog : entrada;
const saida = item.data_saida ? new Date(item.data_saida) : new Date(agora);
const diffHoras = Math.max(0 , saida - entrada) / (1000 * 60 * 60); // ms → horas - Math.max garante que o diff não seja negativo
const aderente = () => {
if(sla) {
if (saida < prog) {
return true
} else {
return diffHoras < target;
}
}
return diffHoras < target;
};
return {
...item,
aderencia: aderente()
};
});
console.log('Dados Filtrados', dadosFiltrados);
//console.log(dadosFiltrados)
renderizarGrafico(dadosFiltrados, sla); // Considerando sla
preencherResumo(dadosFiltrados); // Considerando sla
preencherOfensores(dadosFiltrados);
montarPerdas(dadosFiltrados, agora);
const projecao = calcularProjecao_aderencia(dadosOriginais, dadosFiltrados, 85.1);
const calc_new_meta = calcular_nova_meta(dadosFiltrados, projecao.mediaMensal );
const total_agora = dadosFiltrados.length
document.getElementById('new_meta').innerHTML = `
<h2> ${calc_new_meta}</h2>
`;
document.getElementById('projecao').innerHTML = `
<div class="card destaque projecao">
<strong>Projeção Não Parqueados</strong><br>
Aderência esperada: <b>${projecao.meta}%</b><br>
<br>
<b>Carros até agora:</b> ${total_agora}<br>
<b>Perdas até agora:</b> ${projecao.perdasOcorridas}<br>
<b>Máximo permitido:</b> ${projecao.perdasMaximas}<br>
<b>Média considerada: ${projecao.mediaMensal} ocorrências/mês
</div>
`;
}
// --- calcularMediaTempo (tat_dev) ---
function calcularMediaTempo(dados, sla) {
if (dados.length === 0) return 0;
// quando houver SLA, só considerar casos com saida >= prog (como você faz no gráfico)
const dadosValidos = sla ? dados.filter(it => {
const saida = it.data_saida == '' ? new Date() : new Date(it.data_saida);
return saida >= new Date(it.programacao_dt)
}) : dados;
// Reduce irá trazer a somatoria total de horas
const totalHoras = dadosValidos.reduce((acc, item) => {
const prog = new Date(item.programacao_dt);
const entradaRaw = new Date(item.data_entrada);
const saida = item.data_saida == '' ? new Date() : new Date(item.data_saida);
const entradaCorrigida = sla && entradaRaw < prog ? prog : entradaRaw;
const horas = sla ? (saida >= prog ? calcularTempoPermanencia(entradaCorrigida, saida) : 0)
: calcularTempoPermanencia(entradaCorrigida, saida);
//console.log(acc, horas);
return acc + horas;
}, 0);
return dadosValidos.length ? totalHoras / dadosValidos.length : 0;
}
function calcularAderenciaPercentual(array) {
const total = array.length;
const aderentes = array.filter(item => item.aderencia === true).length;
return total > 0 ? ((aderentes / total) * 100).toFixed(0) : 0;
}
function getClasseAderencia(valor) {
if (valor >= 85) return 'alta';
if (valor >= 70) return 'media';
return 'baixa';
}
function formatarHorasMinutos(valorHoras) {
const horas = Math.floor(valorHoras);
const minutos = Math.round((valorHoras - horas) * 60);
return `${horas}h ${minutos}m`;
}
// Preenche o resumo considerando o flag sla
function preencherResumo(dadosFiltrados, sla) {
const hoje = new Date();
const hojeInicio = new Date(hoje.getFullYear(), hoje.getMonth(), hoje.getDate(), 0, 0, 0);
const hojeFim = new Date(hoje.getFullYear(), hoje.getMonth(), hoje.getDate(), 23, 59, 59);
const dadosHoje = dadosFiltrados.filter(item => {
const agora = new Date();
const saida = new Date(item.data_saida ? item.data_saida : agora);
return saida >= hojeInicio && saida <= hojeFim;
});
const dadosHojeParqueados = dadosHoje.filter(item => item.parqueado);
const dadosHojeNaoParqueados = dadosHoje.filter(item => !item.parqueado);
const mediaHojeGeral = calcularMediaTempo(dadosHoje, bool_sla);
const mediaHojeParqueados = calcularMediaTempo(dadosHojeParqueados, bool_sla);
const mediaHojeNaoParqueados = calcularMediaTempo(dadosHojeNaoParqueados, bool_sla);
//Aderencia Hoje
const aderenciaHojeGeral = calcularAderenciaPercentual(dadosHoje);
const aderenciaHojeParqueados = calcularAderenciaPercentual(dadosHojeParqueados);
const aderenciaHojeNaoParqueados = calcularAderenciaPercentual(dadosHojeNaoParqueados);
// Para Mensal, considerar intervalo completo filtrad
const dadosMesParqueados = dadosFiltrados.filter(item => item.parqueado);
const dadosMesNaoParqueados = dadosFiltrados.filter(item => !item.parqueado);
const mediaMesGeral = calcularMediaTempo(dadosFiltrados, bool_sla);
const mediaMesParqueados = calcularMediaTempo(dadosMesParqueados, bool_sla);
const mediaMesNaoParqueados = calcularMediaTempo(dadosMesNaoParqueados, bool_sla);
// Aderência Mensal
const aderenciaMesGeral = calcularAderenciaPercentual(dadosFiltrados);
const aderenciaMesParqueados = calcularAderenciaPercentual(dadosMesParqueados);
const aderenciaMesNaoParqueados = calcularAderenciaPercentual(dadosMesNaoParqueados);
// Preencher Hoje
document.getElementById('resumo-hoje').innerHTML = `
<div class="card">
<strong>Geral</strong><br>${formatarHorasMinutos(mediaHojeGeral)}
<br>
<span class="aderencia ${getClasseAderencia(aderenciaHojeGeral)}">Aderência: ${aderenciaHojeGeral}%</span>
</div>
<div class="card">
<strong>Parqueados</strong><br>${formatarHorasMinutos(mediaHojeParqueados)}
<br>
<span class="aderencia ${getClasseAderencia(aderenciaHojeParqueados)}">Aderência: ${aderenciaHojeParqueados}%</span>
</div>
<div class="card">
<strong>Não Parqueados</strong><br>${formatarHorasMinutos(mediaHojeNaoParqueados)}
<br>
<span class="aderencia ${getClasseAderencia(aderenciaHojeNaoParqueados)} ">Aderência: ${aderenciaHojeNaoParqueados}%</span>
</div>
`;
// Preencher Mensal
document.getElementById('resumo-mes').innerHTML = `
<div class="card">
<strong>Geral</strong><br>${formatarHorasMinutos(mediaMesGeral)}
<br><span class="aderencia ${getClasseAderencia(aderenciaMesGeral)} ">Aderência: ${aderenciaMesGeral}%</span>
</div>
<div class="card">
<strong>Parqueados</strong><br>${formatarHorasMinutos(mediaMesParqueados)}
<br><span class="aderencia ${getClasseAderencia(aderenciaMesParqueados)}">Aderência: ${aderenciaMesParqueados}%</span>
</div>
<div class="card">
<strong>Não Parqueados</strong><br>${formatarHorasMinutos(mediaMesNaoParqueados)}
<br><span class="aderencia ${getClasseAderencia(aderenciaMesNaoParqueados)} ">Aderência: ${aderenciaMesNaoParqueados}%</span>
</div>
`;
}
function preencherOfensores(dadosFiltrados) {
const hoje = new Date();
const hojeInicio = new Date(hoje.getFullYear(), hoje.getMonth(), hoje.getDate(), 0, 0, 0);
const hojeFim = new Date(hoje.getFullYear(), hoje.getMonth(), hoje.getDate(), 23, 59, 59);
const ofensoresHoje = dadosFiltrados.filter(item => {
const entrada = new Date(item.data_entrada);
const saida = item.data_saida == '' ? null : new Date(item.data_saida);
const permanenciaHoras = calcularTempoPermanencia(entrada, saida);
return (
//entrada >= hojeInicio &&
//entrada <= hojeFim &&
item.parqueado == false &&
!item.data_saida && // Ainda em aberto
permanenciaHoras > 3 // Permanência maior que 5 horas
);
});
// Ordenar ofensores do maior para o menor tempo
ofensoresHoje.sort((a, b) => {
const permanenciaA = calcularTempoPermanencia(new Date(a.data_entrada), null);
const permanenciaB = calcularTempoPermanencia(new Date(b.data_entrada), null);
return permanenciaB - permanenciaA;
});
const listaOfensores = document.getElementById('lista-ofensores');
listaOfensores.innerHTML = '';
if (ofensoresHoje.length === 0) {
listaOfensores.innerHTML = '<p>Sem ofensores no momento.</p>';
} else {
ofensoresHoje.forEach(item => {
const entrada = new Date(item.data_entrada)
const div = document.createElement('div');
var parq = item.parqueado == true ? ' - Parqueado' : ''; // texto para veiculos parqueados
var status = item.status;
var permanenciaHoras = calcularTempoPermanencia(entrada, null);
// O card padrão é Yellow cargas com mais de 5 são criticas, então o card passa a ser red
var cor = null;
if ( permanenciaHoras > 5 ) {
cor = 'red'
}
div.className = 'card-ofensor';
div.classList.add(cor);
div.innerHTML = `
<span>${item.dt} - ${status} - ${formatarHorasMinutos(permanenciaHoras)} ${parq}</span>
`;
listaOfensores.appendChild(div);
});
}
}
function calcular_nova_meta(dados, total_veiculos_considerar ) {
const dadosNaoParqueados = dados.filter(item => !item.parqueado);
const meta = 5;
const total_veiculos_atual = dadosNaoParqueados.length;
const horas_tat_atual = calcularMediaTempo(dados);
//console.log(horas_tat_atual);
const gastoAteAgora = total_veiculos_atual * horas_tat_atual; // TAT total até o momento
if ( total_veiculos_atual <= total_veiculos_considerar ) {
const restante = total_veiculos_considerar - total_veiculos_atual;
const metaTotal = total_veiculos_considerar * meta;
const mediaNecessaria = ( metaTotal - gastoAteAgora) / restante;
if (horas_tat_atual <= meta) {
return [`Sua média atual de ${formatarHorasMinutos(horas_tat_atual)} está abaixo da meta de ${meta}h. Continue assim!`];
}
if (mediaNecessaria < 0) {
return "A meta já foi ultrapassada. Não é possível atingi-la com os atendimentos restantes.";
}
if (mediaNecessaria > meta) {
return "Mesmo zerando o tempo nos próximos atendimentos, não é mais possível atingir a meta.";
}
const horasNecessarias = Math.floor(mediaNecessaria);
const minutosNecessario = Math.floor(mediaNecessaria % 60);
return `Você precisa manter média de até ${formatarHorasMinutos(mediaNecessaria)} nos próximos ${restante} atendimentos para atingir a meta de ${metaHoras}h.`;
}
// Caso de projeção com 4h
const numerador = gastoAteAgora - (meta * total_veiculos_atual);
const divisor = meta - 4;
if (numerador <= 0) {
return "Mesmo com excesso de atendimentos, sua média já está dentro da meta.";
}
if (divisor <= 0) {
return "Parâmetros inválidos. A meta precisa ser maior que 4h para projeção funcionar.";
}
const n = Math.ceil(numerador / divisor);
return `Você precisaria atender aproximadamente ${n} carros a 4h de média para que sua média total fique em ${meta}h.`;
}
function calcularProjecao_aderencia(dadosHistorico, dadosMesAtualComAderencia, meta = 86) {
const agora = new Date();
// Último dia do mês anterior
const fimUltimoMes = new Date(agora.getFullYear(), agora.getMonth(), 0, 23, 59, 59);
// Primeiro dia de 3 meses antes
const inicioTresMesesAtras = new Date(fimUltimoMes.getFullYear(), fimUltimoMes.getMonth() - 2, 1, 0, 0, 0);
// Filtrar últimos 3 meses do histórico
const dadosUltimos3Meses = dadosHistorico.filter(item => {
const dataBase = new Date(item.data_saida || date_f_iso_8601());
return dataBase >= inicioTresMesesAtras && dataBase <= fimUltimoMes && !item.parqueado;
});
// Agrupar por mês
const porMes = {};
dadosUltimos3Meses.forEach(item => {
const dataBase = new Date(item.data_saida || date_f_iso_8601());
const chave = `${dataBase.getFullYear()}-${dataBase.getMonth()}`; // Ex: "2025-6"
if (!porMes[chave]) porMes[chave] = [];
porMes[chave].push(item);
});
const totalMeses = Object.keys(porMes).length;
const somaOcorrencias = Object.values(porMes).reduce((acc, lista) => acc + lista.length, 0);
const mediaOcorrenciasPorMes = totalMeses > 0 ? somaOcorrencias / totalMeses : 0;
const totalProjetadoMesAtual = Math.round(mediaOcorrenciasPorMes,2);
const perdasPermitidas = Math.floor(totalProjetadoMesAtual * (1 - meta / 100));
// Usar os dados filtrados do mês atual para contar as perdas com base na aderência já computada
const perdasOcorridas = dadosMesAtualComAderencia.filter(item => item.aderencia === false).length;
return {
mediaMensal: Math.round(mediaOcorrenciasPorMes),
projetadoMesAtual: totalProjetadoMesAtual,
perdasMaximas: perdasPermitidas,
perdasOcorridas,
meta
};
}
function converterParaCSV(objArray, separador = ",") {
const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray;
// Cabeçalho
const cabecalho = Object.keys(array[0]).join(separador);
// Conteúdo
const linhas = array.map(obj =>
Object.values(obj)
.map(valor => `"${String(valor).replace(/"/g, '""')}"`) // escapa aspas
.join(separador)
);
return [cabecalho, ...linhas].join("\r\n");
}
function baixarCSV(dados, title) {
const csv = converterParaCSV(dados, ";"); // Use ";" se quiser separado por ponto e vírgula
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${title}_tat.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// ===== Botão sla ==== //
function switchsla(btn) {
bool_sla = !bool_sla;
btn.className = bool_sla ? 'active' : 'deactivate';
btn.textContent = bool_sla ? 'SLA - Ativado' : 'SLA - Desativado';
aplicarFiltros(bool_sla);
}