feat: enhance vacation management system with new employee association functionality, improved email notification handling, and comprehensive documentation; update dependencies and UI components for better user experience

This commit is contained in:
2025-10-30 09:27:10 -03:00
parent 21b41121db
commit fd445e8246
43 changed files with 6097 additions and 515 deletions

View File

@@ -0,0 +1,394 @@
<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { onMount } from "svelte";
interface Props {
funcionarioId: Id<"funcionarios">;
}
let { funcionarioId }: Props = $props();
// Queries
const saldosQuery = useQuery(api.saldoFerias.listarSaldos, { funcionarioId });
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, { funcionarioId });
const saldos = $derived(saldosQuery.data || []);
const solicitacoes = $derived(solicitacoesQuery.data || []);
// Estatísticas derivadas
const saldoAtual = $derived(saldos.find((s) => s.anoReferencia === new Date().getFullYear()));
const totalSolicitacoes = $derived(solicitacoes.length);
const aprovadas = $derived(solicitacoes.filter((s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length);
const pendentes = $derived(solicitacoes.filter((s) => s.status === "aguardando_aprovacao").length);
const reprovadas = $derived(solicitacoes.filter((s) => s.status === "reprovado").length);
// Canvas para gráfico de pizza
let canvasSaldo = $state<HTMLCanvasElement>();
let canvasStatus = $state<HTMLCanvasElement>();
// Função para desenhar gráfico de pizza moderno
function desenharGraficoPizza(
canvas: HTMLCanvasElement,
dados: { label: string; valor: number; cor: string }[]
) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 20;
ctx.clearRect(0, 0, width, height);
const total = dados.reduce((acc, d) => acc + d.valor, 0);
if (total === 0) return;
let startAngle = -Math.PI / 2;
dados.forEach((item) => {
const sliceAngle = (2 * Math.PI * item.valor) / total;
// Desenhar fatia com sombra
ctx.save();
ctx.shadowColor = "rgba(0, 0, 0, 0.2)";
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = item.cor;
ctx.fill();
ctx.restore();
// Desenhar borda branca
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 3;
ctx.stroke();
startAngle += sliceAngle;
});
// Desenhar círculo branco no centro (efeito donut)
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.6, 0, 2 * Math.PI);
ctx.fillStyle = "#ffffff";
ctx.fill();
}
// Atualizar gráficos quando dados mudarem
$effect(() => {
if (canvasSaldo && saldoAtual) {
desenharGraficoPizza(canvasSaldo, [
{ label: "Usado", valor: saldoAtual.diasUsados, cor: "#ff6b6b" },
{ label: "Pendente", valor: saldoAtual.diasPendentes, cor: "#ffa94d" },
{ label: "Disponível", valor: saldoAtual.diasDisponiveis, cor: "#51cf66" },
]);
}
if (canvasStatus && totalSolicitacoes > 0) {
desenharGraficoPizza(canvasStatus, [
{ label: "Aprovadas", valor: aprovadas, cor: "#51cf66" },
{ label: "Pendentes", valor: pendentes, cor: "#ffa94d" },
{ label: "Reprovadas", valor: reprovadas, cor: "#ff6b6b" },
]);
}
});
</script>
<div class="dashboard-ferias">
<!-- Header -->
<div class="mb-8">
<h1 class="text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
📊 Dashboard de Férias
</h1>
<p class="text-base-content/70 mt-2">Visualize seus saldos e histórico de solicitações</p>
</div>
{#if saldosQuery.isLoading || solicitacoesQuery.isLoading}
<!-- Loading Skeletons -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{#each Array(4) as _}
<div class="skeleton h-32 rounded-2xl"></div>
{/each}
</div>
{:else}
<!-- Cards de Estatísticas -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Card 1: Saldo Disponível -->
<div
class="stat bg-gradient-to-br from-success/20 to-success/5 border-2 border-success/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<div class="stat-title text-success font-semibold">Disponível</div>
<div class="stat-value text-success text-4xl">{saldoAtual?.diasDisponiveis || 0}</div>
<div class="stat-desc text-success/70">dias para usar</div>
</div>
<!-- Card 2: Dias Usados -->
<div
class="stat bg-gradient-to-br from-error/20 to-error/5 border-2 border-error/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-error">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
<div class="stat-title text-error font-semibold">Usado</div>
<div class="stat-value text-error text-4xl">{saldoAtual?.diasUsados || 0}</div>
<div class="stat-desc text-error/70">dias já gozados</div>
</div>
<!-- Card 3: Pendentes -->
<div
class="stat bg-gradient-to-br from-warning/20 to-warning/5 border-2 border-warning/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<div class="stat-title text-warning font-semibold">Pendentes</div>
<div class="stat-value text-warning text-4xl">{saldoAtual?.diasPendentes || 0}</div>
<div class="stat-desc text-warning/70">aguardando aprovação</div>
</div>
<!-- Card 4: Total de Direito -->
<div
class="stat bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div class="stat-title text-primary font-semibold">Total Direito</div>
<div class="stat-value text-primary text-4xl">{saldoAtual?.diasDireito || 0}</div>
<div class="stat-desc text-primary/70">dias no ano</div>
</div>
</div>
<!-- Gráficos -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Gráfico 1: Distribuição de Saldo -->
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
🥧 Distribuição de Saldo
<div class="badge badge-primary badge-lg">
Ano {saldoAtual?.anoReferencia || new Date().getFullYear()}
</div>
</h2>
{#if saldoAtual}
<div class="flex items-center justify-center">
<canvas
bind:this={canvasSaldo}
width="300"
height="300"
class="max-w-full"
></canvas>
</div>
<!-- Legenda -->
<div class="flex justify-center gap-4 mt-4 flex-wrap">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold">Disponível: {saldoAtual.diasDisponiveis} dias</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold">Pendente: {saldoAtual.diasPendentes} dias</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold">Usado: {saldoAtual.diasUsados} dias</span>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhum saldo disponível para o ano atual</span>
</div>
{/if}
</div>
</div>
<!-- Gráfico 2: Status de Solicitações -->
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
📋 Status de Solicitações
<div class="badge badge-secondary badge-lg">Total: {totalSolicitacoes}</div>
</h2>
{#if totalSolicitacoes > 0}
<div class="flex items-center justify-center">
<canvas
bind:this={canvasStatus}
width="300"
height="300"
class="max-w-full"
></canvas>
</div>
<!-- Legenda -->
<div class="flex justify-center gap-4 mt-4 flex-wrap">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold">Pendentes: {pendentes}</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold">Reprovadas: {reprovadas}</span>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhuma solicitação de férias ainda</span>
</div>
{/if}
</div>
</div>
</div>
<!-- Histórico de Saldos -->
{#if saldos.length > 0}
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">📅 Histórico de Saldos</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Ano</th>
<th>Direito</th>
<th>Usado</th>
<th>Pendente</th>
<th>Disponível</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each saldos as saldo}
<tr>
<td class="font-bold">{saldo.anoReferencia}</td>
<td>{saldo.diasDireito} dias</td>
<td><span class="badge badge-error">{saldo.diasUsados}</span></td>
<td><span class="badge badge-warning">{saldo.diasPendentes}</span></td>
<td><span class="badge badge-success">{saldo.diasDisponiveis}</span></td>
<td>
{#if saldo.status === "ativo"}
<span class="badge badge-success">Ativo</span>
{:else if saldo.status === "vencido"}
<span class="badge badge-error">Vencido</span>
{:else}
<span class="badge badge-neutral">Concluído</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
{/if}
</div>
<style>
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
</style>