feat: add area chart for upcoming employee leave data, visualizing monthly vacation counts and enhancing dashboard insights
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
import type { FunctionReturnType } from 'convex/server';
|
import type { FunctionReturnType } from 'convex/server';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte';
|
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 AlterarStatusFerias from '$lib/components/AlterarStatusFerias.svelte';
|
||||||
import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte';
|
import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte';
|
||||||
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
|
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
|
||||||
@@ -529,6 +530,108 @@
|
|||||||
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
|
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 = [
|
const coresCalendario = [
|
||||||
'#2563eb',
|
'#2563eb',
|
||||||
'#16a34a',
|
'#16a34a',
|
||||||
@@ -2342,6 +2445,46 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -82,8 +82,11 @@ export const listarTodas = query({
|
|||||||
|
|
||||||
// Buscar usuário do funcionário para obter fotoPerfilUrl
|
// Buscar usuário do funcionário para obter fotoPerfilUrl
|
||||||
let fotoPerfilUrl: string | null = null;
|
let fotoPerfilUrl: string | null = null;
|
||||||
if (funcionario?.usuarioId) {
|
if (funcionario) {
|
||||||
const usuario = await ctx.db.get(funcionario.usuarioId);
|
const usuario = await ctx.db
|
||||||
|
.query("usuarios")
|
||||||
|
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
||||||
|
.first();
|
||||||
if (usuario?.fotoPerfil) {
|
if (usuario?.fotoPerfil) {
|
||||||
fotoPerfilUrl = await ctx.storage.getUrl(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))
|
.filter((q) => q.eq(q.field("ativo"), true))
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
let time = null;
|
let time: Doc<"times"> | null = null;
|
||||||
let gestor = null;
|
let gestor: { _id: Id<"usuarios">; nome: string } | null = null;
|
||||||
if (membroTime) {
|
if (membroTime) {
|
||||||
time = await ctx.db.get(membroTime.timeId);
|
time = await ctx.db.get(membroTime.timeId);
|
||||||
// Buscar gestor do time
|
// Buscar gestor do time
|
||||||
if (time?.gestorId) {
|
if (time?.gestorId) {
|
||||||
const gestorUsuario = await ctx.db.get(time.gestorId);
|
const gestorUsuario = await ctx.db.get(time.gestorId);
|
||||||
if (gestorUsuario) {
|
if (gestorUsuario?.funcionarioId) {
|
||||||
// Buscar funcionário do gestor para obter o nome
|
// Buscar funcionário do gestor para obter o nome
|
||||||
const gestorFuncionario = await ctx.db
|
const gestorFuncionario = await ctx.db.get(gestorUsuario.funcionarioId);
|
||||||
.query("funcionarios")
|
|
||||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", time.gestorId))
|
|
||||||
.first();
|
|
||||||
if (gestorFuncionario) {
|
if (gestorFuncionario) {
|
||||||
gestor = {
|
gestor = {
|
||||||
_id: gestorUsuario._id,
|
_id: gestorUsuario._id,
|
||||||
|
|||||||
@@ -871,6 +871,10 @@ export default defineSchema({
|
|||||||
configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor
|
configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor
|
||||||
configuradoPor: v.id("usuarios"), // Usuário que configurou
|
configuradoPor: v.id("usuarios"), // Usuário que configurou
|
||||||
atualizadoEm: v.number(), // Timestamp de atualização
|
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"]),
|
}).index("by_ativo", ["ativo"]),
|
||||||
|
|
||||||
// Fila de Emails
|
// Fila de Emails
|
||||||
|
|||||||
Reference in New Issue
Block a user