From 545e119367e994c327885d1244086a79e16fa6f1 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 29 Nov 2025 22:27:23 -0300 Subject: [PATCH] feat: add area chart for upcoming employee leave data, visualizing monthly vacation counts and enhancing dashboard insights --- .../recursos-humanos/ferias/+page.svelte | 143 ++++++++++++++++++ packages/backend/convex/ferias.ts | 18 +-- packages/backend/convex/schema.ts | 4 + 3 files changed, 156 insertions(+), 9 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte index 666feb8..dbe2863 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte @@ -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(); + + 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 @@ {:else}
+ +
+
+
+
+ + + +
+
+

+ Funcionários de Férias - Próximos 12 Meses +

+

+ Quantitativo de funcionários que estarão de férias mês a mês +

+
+
+
+ {#if chartDataFuncionariosFerias.labels.length === 0} +
+

Sem dados registrados até o momento.

+
+ {:else} + + {/if} +
+
+
{/if} {:else if abaAtiva === 'solicitacoes'} diff --git a/packages/backend/convex/ferias.ts b/packages/backend/convex/ferias.ts index c071260..747fa31 100644 --- a/packages/backend/convex/ferias.ts +++ b/packages/backend/convex/ferias.ts @@ -82,8 +82,11 @@ export const listarTodas = query({ // Buscar usuário do funcionário para obter fotoPerfilUrl let fotoPerfilUrl: string | null = null; - if (funcionario?.usuarioId) { - const usuario = await ctx.db.get(funcionario.usuarioId); + if (funcionario) { + const usuario = await ctx.db + .query("usuarios") + .withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id)) + .first(); if (usuario?.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } @@ -98,19 +101,16 @@ export const listarTodas = query({ .filter((q) => q.eq(q.field("ativo"), true)) .first(); - let time = null; - let gestor = null; + let time: Doc<"times"> | null = null; + let gestor: { _id: Id<"usuarios">; nome: string } | null = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); // Buscar gestor do time if (time?.gestorId) { const gestorUsuario = await ctx.db.get(time.gestorId); - if (gestorUsuario) { + if (gestorUsuario?.funcionarioId) { // Buscar funcionário do gestor para obter o nome - const gestorFuncionario = await ctx.db - .query("funcionarios") - .withIndex("by_usuario", (q) => q.eq("usuarioId", time.gestorId)) - .first(); + const gestorFuncionario = await ctx.db.get(gestorUsuario.funcionarioId); if (gestorFuncionario) { gestor = { _id: gestorUsuario._id, diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index a02147e..21afec4 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -871,6 +871,10 @@ export default defineSchema({ configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor configuradoPor: v.id("usuarios"), // Usuário que configurou atualizadoEm: v.number(), // Timestamp de atualização + jitsiConfigPath: v.optional(v.string()), // Caminho da configuração do Jitsi no servidor (ex: "~/.jitsi-meet-cfg") + sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor + sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada) + sshPort: v.optional(v.number()), // Porta SSH (padrão: 22) }).index("by_ativo", ["ativo"]), // Fila de Emails