feat: add area chart for upcoming employee leave data, visualizing monthly vacation counts and enhancing dashboard insights

This commit is contained in:
2025-11-29 22:27:23 -03:00
parent 1d9f924cb8
commit 545e119367
3 changed files with 156 additions and 9 deletions

View File

@@ -8,6 +8,7 @@
import type { FunctionReturnType } from 'convex/server';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte';
import AreaChart from '$lib/components/ti/charts/AreaChart.svelte';
import AlterarStatusFerias from '$lib/components/AlterarStatusFerias.svelte';
import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte';
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
@@ -529,6 +530,108 @@
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
}
// Dados para gráfico de área - Funcionários de férias nos próximos 12 meses
const chartDataFuncionariosFerias = $derived(() => {
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
// Criar array com os próximos 12 meses
const meses: Array<{ mes: string; dataInicio: Date; dataFim: Date; quantidade: number }> = [];
for (let i = 0; i < 12; i++) {
const dataInicioMes = new Date(hoje.getFullYear(), hoje.getMonth() + i, 1);
const dataFimMes = new Date(hoje.getFullYear(), hoje.getMonth() + i + 1, 0);
dataFimMes.setHours(23, 59, 59, 999);
const mesLabel = format(dataInicioMes, 'MMM/yyyy', { locale: ptBR });
meses.push({
mes: mesLabel,
dataInicio: dataInicioMes,
dataFim: dataFimMes,
quantidade: 0
});
}
// Filtrar apenas solicitações aprovadas que estão ou estarão em férias
const solicitacoesAprovadas = ultimasSolicitacoesValidas.filter(
(s) =>
s.status === 'aprovado' ||
s.status === 'data_ajustada_aprovada' ||
s.status === 'EmFérias'
);
// Calcular quantos funcionários estarão de férias em cada mês
meses.forEach((mesInfo) => {
const funcionariosEmFerias = new Set<string>();
solicitacoesAprovadas.forEach((solicitacao) => {
if (!solicitacao.funcionarioId) return;
const dataInicio = new Date(solicitacao.dataInicio);
const dataFim = new Date(solicitacao.dataFim);
dataInicio.setHours(0, 0, 0, 0);
dataFim.setHours(23, 59, 59, 999);
// Verificar se o período de férias se sobrepõe com o mês
if (
(dataInicio <= mesInfo.dataFim && dataFim >= mesInfo.dataInicio)
) {
funcionariosEmFerias.add(String(solicitacao.funcionarioId));
}
});
mesInfo.quantidade = funcionariosEmFerias.size;
});
// Cores harmoniosas com o tema (gradiente de azul primary para accent)
const corBase = '#3b82f6'; // Azul primary
const corAccent = '#8b5cf6'; // Roxo accent
// Criar gradiente de cores harmonioso
const cores = meses.map((_, index) => {
const ratio = meses.length > 1 ? index / (meses.length - 1) : 0;
const r1 = parseInt(corBase.slice(1, 3), 16);
const g1 = parseInt(corBase.slice(3, 5), 16);
const b1 = parseInt(corBase.slice(5, 7), 16);
const r2 = parseInt(corAccent.slice(1, 3), 16);
const g2 = parseInt(corAccent.slice(3, 5), 16);
const b2 = parseInt(corAccent.slice(5, 7), 16);
const r = Math.round(r1 + (r2 - r1) * ratio);
const g = Math.round(g1 + (g2 - g1) * ratio);
const b = Math.round(b1 + (b2 - b1) * ratio);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
});
// Cor principal para a borda (mais vibrante)
const corBorda = corBase;
return {
labels: meses.map((m) => m.mes),
datasets: [
{
label: 'Funcionários de Férias',
data: meses.map((m) => m.quantidade),
backgroundColor: cores.map((cor) => `${cor}60`), // 37.5% de opacidade para preenchimento
borderColor: corBorda,
borderWidth: 3,
pointBackgroundColor: cores,
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 5,
pointHoverRadius: 7,
pointHoverBackgroundColor: corBorda,
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 3,
fill: true,
tension: 0.4,
spanGaps: false
}
]
};
});
const coresCalendario = [
'#2563eb',
'#16a34a',
@@ -2342,6 +2445,46 @@
{/each}
</div>
{:else}
<div class="mb-6 space-y-6" bind:this={chartContainer}>
<!-- Gráfico de Funcionários de Férias - Próximos 12 Meses -->
<div class="card bg-base-100 border-accent/20 border shadow-lg">
<div class="card-body space-y-4 p-6">
<div class="flex items-center gap-3">
<div class="bg-accent/10 rounded-lg p-2.5">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-accent h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
<div>
<h3 class="text-base-content text-lg font-semibold">
Funcionários de Férias - Próximos 12 Meses
</h3>
<p class="text-base-content/60 text-sm">
Quantitativo de funcionários que estarão de férias mês a mês
</p>
</div>
</div>
<div class="bg-base-200/40 w-full overflow-x-auto rounded-xl p-4">
{#if chartDataFuncionariosFerias.labels.length === 0}
<div class="flex h-96 items-center justify-center">
<p class="text-base-content/60">Sem dados registrados até o momento.</p>
</div>
{:else}
<AreaChart data={chartDataFuncionariosFerias} height={400} />
{/if}
</div>
</div>
</div>
</div>
{/if}