Correcao ferias #23

Merged
deyvisonwanderley merged 3 commits from correcao-ferias into master 2025-11-14 12:24:50 +00:00
12 changed files with 1185 additions and 962 deletions

View File

@@ -3,13 +3,14 @@
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
type SolicitacaoFerias = Doc<'solicitacoesFerias'> & { type PeriodoFerias = Doc<'ferias'> & {
funcionario?: Doc<'funcionarios'> | null; funcionario?: Doc<'funcionarios'> | null;
gestor?: Doc<'usuarios'> | null; gestor?: Doc<'usuarios'> | null;
time?: Doc<'times'> | null;
}; };
interface Props { interface Props {
solicitacao: SolicitacaoFerias; solicitacao: PeriodoFerias;
usuarioId: Id<'usuarios'>; usuarioId: Id<'usuarios'>;
onSucesso?: () => void; onSucesso?: () => void;
onCancelar?: () => void; onCancelar?: () => void;
@@ -27,7 +28,8 @@
aguardando_aprovacao: 'badge-warning', aguardando_aprovacao: 'badge-warning',
aprovado: 'badge-success', aprovado: 'badge-success',
reprovado: 'badge-error', reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info' data_ajustada_aprovada: 'badge-info',
EmFérias: 'badge-info'
}; };
return badges[status] || 'badge-neutral'; return badges[status] || 'badge-neutral';
} }
@@ -37,7 +39,8 @@
aguardando_aprovacao: 'Aguardando Aprovação', aguardando_aprovacao: 'Aguardando Aprovação',
aprovado: 'Aprovado', aprovado: 'Aprovado',
reprovado: 'Reprovado', reprovado: 'Reprovado',
data_ajustada_aprovada: 'Data Ajustada e Aprovada' data_ajustada_aprovada: 'Data Ajustada e Aprovada',
EmFérias: 'Em Férias'
}; };
return textos[status] || status; return textos[status] || status;
} }
@@ -48,7 +51,7 @@
erro = ''; erro = '';
await client.mutation(api.ferias.atualizarStatus, { await client.mutation(api.ferias.atualizarStatus, {
solicitacaoId: solicitacao._id, feriasId: solicitacao._id,
novoStatus: 'aguardando_aprovacao', novoStatus: 'aguardando_aprovacao',
usuarioId: usuarioId usuarioId: usuarioId
}); });
@@ -82,33 +85,28 @@
</div> </div>
</div> </div>
<!-- Períodos Solicitados --> <!-- Período Solicitado -->
<div class="mt-4"> <div class="mt-4">
<h3 class="mb-3 text-lg font-semibold">Períodos Solicitados</h3> <h3 class="mb-3 text-lg font-semibold">Período Solicitado</h3>
<div class="space-y-2"> <div class="bg-base-200 rounded-lg p-4">
{#each solicitacao.periodos as periodo, index (index)} <div class="grid grid-cols-3 gap-4 text-sm">
<div class="bg-base-200 flex items-center gap-4 rounded-lg p-3"> <div>
<div class="badge badge-primary">{index + 1}</div> <span class="text-base-content/70">Início:</span>
<div class="grid flex-1 grid-cols-3 gap-2 text-sm"> <span class="ml-1 font-semibold"
<div> >{new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')}</span
<span class="text-base-content/70">Início:</span> >
<span class="ml-1 font-semibold"
>{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')}</span
>
</div>
<div>
<span class="text-base-content/70">Fim:</span>
<span class="ml-1 font-semibold"
>{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}</span
>
</div>
<div>
<span class="text-base-content/70">Dias:</span>
<span class="text-primary ml-1 font-bold">{periodo.diasCorridos}</span>
</div>
</div>
</div> </div>
{/each} <div>
<span class="text-base-content/70">Fim:</span>
<span class="ml-1 font-semibold"
>{new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}</span
>
</div>
<div>
<span class="text-base-content/70">Dias:</span>
<span class="text-primary ml-1 font-bold">{solicitacao.diasFerias}</span>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -3,59 +3,55 @@
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
interface Periodo { type PeriodoFerias = Doc<'ferias'> & {
dataInicio: string;
dataFim: string;
diasCorridos: number;
}
type SolicitacaoFerias = Doc<'solicitacoesFerias'> & {
funcionario?: Doc<'funcionarios'> | null; funcionario?: Doc<'funcionarios'> | null;
gestor?: Doc<'usuarios'> | null; gestor?: Doc<'usuarios'> | null;
time?: Doc<'times'> | null;
}; };
interface Props { interface Props {
solicitacao: SolicitacaoFerias; periodo: PeriodoFerias;
gestorId: Id<'usuarios'>; gestorId: Id<'usuarios'>;
onSucesso?: () => void; onSucesso?: () => void;
onCancelar?: () => void; onCancelar?: () => void;
} }
let { solicitacao, gestorId, onSucesso, onCancelar }: Props = $props(); let { periodo, gestorId, onSucesso, onCancelar }: Props = $props();
const client = useConvexClient(); const client = useConvexClient();
let modoAjuste = $state(false); let modoAjuste = $state(false);
let periodos = $state<Periodo[]>([]); let novaDataInicio = $state(periodo.dataInicio);
let novaDataFim = $state(periodo.dataFim);
let motivoReprovacao = $state(''); let motivoReprovacao = $state('');
let processando = $state(false); let processando = $state(false);
let erro = $state(''); let erro = $state('');
$effect(() => { // Calcular dias do período ajustado
if (modoAjuste && periodos.length === 0) { const diasAjustados = $derived.by(() => {
periodos = solicitacao.periodos.map((p) => ({ ...p })); if (!novaDataInicio || !novaDataFim) return 0;
} const inicio = new Date(novaDataInicio);
const fim = new Date(novaDataFim);
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays;
}); });
function calcularDias(periodo: Periodo) { function calcularDias(dataInicio: string, dataFim: string): number {
if (!periodo.dataInicio || !periodo.dataFim) { if (!dataInicio || !dataFim) return 0;
periodo.diasCorridos = 0;
return;
}
const inicio = new Date(periodo.dataInicio); const inicio = new Date(dataInicio);
const fim = new Date(periodo.dataFim); const fim = new Date(dataFim);
if (fim < inicio) { if (fim < inicio) {
erro = 'Data final não pode ser anterior à data inicial'; erro = 'Data final não pode ser anterior à data inicial';
periodo.diasCorridos = 0; return 0;
return;
} }
const diff = fim.getTime() - inicio.getTime(); const diff = fim.getTime() - inicio.getTime();
const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1; const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
periodo.diasCorridos = dias;
erro = ''; erro = '';
return dias;
} }
async function aprovar() { async function aprovar() {
@@ -64,19 +60,19 @@
erro = ''; erro = '';
// Validar se as datas e condições estão dentro do regime do funcionário // Validar se as datas e condições estão dentro do regime do funcionário
if (!solicitacao.funcionario?._id) { if (!periodo.funcionario?._id) {
erro = 'Funcionário não encontrado'; erro = 'Funcionário não encontrado';
processando = false; processando = false;
return; return;
} }
const validacao = await client.query(api.saldoFerias.validarSolicitacao, { const validacao = await client.query(api.saldoFerias.validarSolicitacao, {
funcionarioId: solicitacao.funcionario._id, funcionarioId: periodo.funcionario._id,
anoReferencia: solicitacao.anoReferencia, anoReferencia: periodo.anoReferencia,
periodos: solicitacao.periodos.map((p) => ({ periodos: [{
dataInicio: p.dataInicio, dataInicio: periodo.dataInicio,
dataFim: p.dataFim dataFim: periodo.dataFim
})) }]
}); });
if (!validacao.valido) { if (!validacao.valido) {
@@ -86,7 +82,7 @@
} }
await client.mutation(api.ferias.aprovar, { await client.mutation(api.ferias.aprovar, {
solicitacaoId: solicitacao._id, feriasId: periodo._id,
gestorId: gestorId gestorId: gestorId
}); });
@@ -109,7 +105,7 @@
erro = ''; erro = '';
await client.mutation(api.ferias.reprovar, { await client.mutation(api.ferias.reprovar, {
solicitacaoId: solicitacao._id, feriasId: periodo._id,
gestorId: gestorId, gestorId: gestorId,
motivoReprovacao motivoReprovacao
}); });
@@ -128,28 +124,27 @@
erro = ''; erro = '';
// Validar se as datas ajustadas e condições estão dentro do regime do funcionário // Validar se as datas ajustadas e condições estão dentro do regime do funcionário
if (!solicitacao.funcionario?._id) { if (!periodo.funcionario?._id) {
erro = 'Funcionário não encontrado'; erro = 'Funcionário não encontrado';
processando = false; processando = false;
return; return;
} }
// Validar todos os períodos ajustados // Validar datas ajustadas
for (const periodo of periodos) { if (!novaDataInicio || !novaDataFim) {
if (!periodo.dataInicio || !periodo.dataFim) { erro = 'Informe as novas datas de início e fim';
erro = 'Todos os períodos devem ter data de início e fim'; processando = false;
processando = false; return;
return;
}
} }
const validacao = await client.query(api.saldoFerias.validarSolicitacao, { const validacao = await client.query(api.saldoFerias.validarSolicitacao, {
funcionarioId: solicitacao.funcionario._id, funcionarioId: periodo.funcionario._id,
anoReferencia: solicitacao.anoReferencia, anoReferencia: periodo.anoReferencia,
periodos: periodos.map((p) => ({ periodos: [{
dataInicio: p.dataInicio, dataInicio: novaDataInicio,
dataFim: p.dataFim dataFim: novaDataFim
})) }],
feriasIdExcluir: periodo._id // Excluir o período original do cálculo de saldo
}); });
if (!validacao.valido) { if (!validacao.valido) {
@@ -159,9 +154,10 @@
} }
await client.mutation(api.ferias.ajustarEAprovar, { await client.mutation(api.ferias.ajustarEAprovar, {
solicitacaoId: solicitacao._id, feriasId: periodo._id,
gestorId: gestorId, gestorId: gestorId,
novosPeriodos: periodos novaDataInicio,
novaDataFim
}); });
if (onSucesso) onSucesso(); if (onSucesso) onSucesso();
@@ -177,7 +173,8 @@
aguardando_aprovacao: 'badge-warning', aguardando_aprovacao: 'badge-warning',
aprovado: 'badge-success', aprovado: 'badge-success',
reprovado: 'badge-error', reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info' data_ajustada_aprovada: 'badge-info',
EmFérias: 'badge-info'
}; };
return badges[status] || 'badge-neutral'; return badges[status] || 'badge-neutral';
} }
@@ -187,7 +184,8 @@
aguardando_aprovacao: 'Aguardando Aprovação', aguardando_aprovacao: 'Aguardando Aprovação',
aprovado: 'Aprovado', aprovado: 'Aprovado',
reprovado: 'Reprovado', reprovado: 'Reprovado',
data_ajustada_aprovada: 'Data Ajustada e Aprovada' data_ajustada_aprovada: 'Data Ajustada e Aprovada',
EmFérias: 'Em Férias'
}; };
return textos[status] || status; return textos[status] || status;
} }
@@ -195,6 +193,23 @@
function formatarData(data: number) { function formatarData(data: number) {
return new Date(data).toLocaleString('pt-BR'); return new Date(data).toLocaleString('pt-BR');
} }
// Função para formatar data sem problemas de timezone
function formatarDataString(dataString: string): string {
if (!dataString) return '';
// Dividir a string da data (formato YYYY-MM-DD)
const partes = dataString.split('-');
if (partes.length !== 3) return dataString;
// Retornar no formato DD/MM/YYYY
return `${partes[2]}/${partes[1]}/${partes[0]}`;
}
$effect(() => {
if (modoAjuste) {
novaDataInicio = periodo.dataInicio;
novaDataFim = periodo.dataFim;
}
});
</script> </script>
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
@@ -202,63 +217,58 @@
<div class="mb-4 flex items-start justify-between"> <div class="mb-4 flex items-start justify-between">
<div> <div>
<h2 class="card-title text-2xl"> <h2 class="card-title text-2xl">
{solicitacao.funcionario?.nome || 'Funcionário'} {periodo.funcionario?.nome || 'Funcionário'}
</h2> </h2>
<p class="text-base-content/70 mt-1 text-sm"> <p class="text-base-content/70 mt-1 text-sm">
Ano de Referência: {solicitacao.anoReferencia} Ano de Referência: {periodo.anoReferencia}
</p> </p>
</div> </div>
<div class={`badge ${getStatusBadge(solicitacao.status)} badge-lg`}> <div class={`badge ${getStatusBadge(periodo.status)} badge-lg`}>
{getStatusTexto(solicitacao.status)} {getStatusTexto(periodo.status)}
</div> </div>
</div> </div>
<!-- Períodos Solicitados --> <!-- Período Solicitado -->
<div class="mt-4"> <div class="mt-4">
<h3 class="mb-3 text-lg font-semibold">Períodos Solicitados</h3> <h3 class="mb-3 text-lg font-semibold">Período Solicitado</h3>
<div class="space-y-2"> <div class="bg-base-200 rounded-lg p-4">
{#each solicitacao.periodos as periodo, index} <div class="grid grid-cols-3 gap-4 text-sm">
<div class="bg-base-200 flex items-center gap-4 rounded-lg p-3"> <div>
<div class="badge badge-primary">{index + 1}</div> <span class="text-base-content/70">Início:</span>
<div class="grid flex-1 grid-cols-3 gap-2 text-sm"> <span class="ml-1 font-semibold"
<div> >{formatarDataString(periodo.dataInicio)}</span
<span class="text-base-content/70">Início:</span> >
<span class="ml-1 font-semibold"
>{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')}</span
>
</div>
<div>
<span class="text-base-content/70">Fim:</span>
<span class="ml-1 font-semibold"
>{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}</span
>
</div>
<div>
<span class="text-base-content/70">Dias:</span>
<span class="text-primary ml-1 font-bold">{periodo.diasCorridos}</span>
</div>
</div>
</div> </div>
{/each} <div>
<span class="text-base-content/70">Fim:</span>
<span class="ml-1 font-semibold"
>{formatarDataString(periodo.dataFim)}</span
>
</div>
<div>
<span class="text-base-content/70">Dias:</span>
<span class="text-primary ml-1 font-bold">{periodo.diasFerias}</span>
</div>
</div>
</div> </div>
</div> </div>
<!-- Observações --> <!-- Observações -->
{#if solicitacao.observacao} {#if periodo.observacao}
<div class="mt-4"> <div class="mt-4">
<h3 class="mb-2 font-semibold">Observações</h3> <h3 class="mb-2 font-semibold">Observações</h3>
<div class="bg-base-200 rounded-lg p-3 text-sm"> <div class="bg-base-200 rounded-lg p-3 text-sm">
{solicitacao.observacao} {periodo.observacao}
</div> </div>
</div> </div>
{/if} {/if}
<!-- Histórico --> <!-- Histórico -->
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0} {#if periodo.historicoAlteracoes && periodo.historicoAlteracoes.length > 0}
<div class="mt-4"> <div class="mt-4">
<h3 class="mb-2 font-semibold">Histórico</h3> <h3 class="mb-2 font-semibold">Histórico</h3>
<div class="space-y-1"> <div class="space-y-1">
{#each solicitacao.historicoAlteracoes as hist} {#each periodo.historicoAlteracoes as hist}
<div class="text-base-content/70 flex items-center gap-2 text-xs"> <div class="text-base-content/70 flex items-center gap-2 text-xs">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -284,7 +294,7 @@
{/if} {/if}
<!-- Ações (apenas para status aguardando_aprovacao) --> <!-- Ações (apenas para status aguardando_aprovacao) -->
{#if solicitacao.status === 'aguardando_aprovacao'} {#if periodo.status === 'aguardando_aprovacao'}
<div class="divider mt-6"></div> <div class="divider mt-6"></div>
{#if !modoAjuste} {#if !modoAjuste}
@@ -341,7 +351,7 @@
<!-- Reprovar --> <!-- Reprovar -->
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-4"> <div class="card-body p-4">
<h4 class="mb-2 text-sm font-semibold">Reprovar Solicitação</h4> <h4 class="mb-2 text-sm font-semibold">Reprovar Período</h4>
<textarea <textarea
class="textarea textarea-bordered textarea-sm mb-2" class="textarea textarea-bordered textarea-sm mb-2"
placeholder="Motivo da reprovação..." placeholder="Motivo da reprovação..."
@@ -376,53 +386,49 @@
{:else} {:else}
<!-- Modo Ajuste --> <!-- Modo Ajuste -->
<div class="space-y-4"> <div class="space-y-4">
<h4 class="font-semibold">Ajustar Períodos</h4> <h4 class="font-semibold">Ajustar Período</h4>
{#each periodos as periodo, index} <div class="card bg-base-200">
<div class="card bg-base-200"> <div class="card-body p-4">
<div class="card-body p-4"> <div class="grid grid-cols-3 gap-3">
<h5 class="mb-2 font-medium">Período {index + 1}</h5> <div class="form-control">
<div class="grid grid-cols-3 gap-3"> <label class="label" for="ajuste-inicio">
<div class="form-control"> <span class="label-text text-xs">Início</span>
<label class="label" for={`ajuste-inicio-${index}`}> </label>
<span class="label-text text-xs">Início</span> <input
</label> id="ajuste-inicio"
<input type="date"
id={`ajuste-inicio-${index}`} class="input input-bordered input-sm"
type="date" bind:value={novaDataInicio}
class="input input-bordered input-sm" />
bind:value={periodo.dataInicio} </div>
onchange={() => calcularDias(periodo)} <div class="form-control">
/> <label class="label" for="ajuste-fim">
</div> <span class="label-text text-xs">Fim</span>
<div class="form-control"> </label>
<label class="label" for={`ajuste-fim-${index}`}> <input
<span class="label-text text-xs">Fim</span> id="ajuste-fim"
</label> type="date"
<input class="input input-bordered input-sm"
id={`ajuste-fim-${index}`} bind:value={novaDataFim}
type="date" />
class="input input-bordered input-sm" </div>
bind:value={periodo.dataFim} <div class="form-control">
onchange={() => calcularDias(periodo)} <label class="label" for="ajuste-dias">
/> <span class="label-text text-xs">Dias</span>
</div> </label>
<div class="form-control"> <div
<label class="label" for={`ajuste-dias-${index}`}> id="ajuste-dias"
<span class="label-text text-xs">Dias</span> class="bg-base-300 flex h-9 items-center rounded-lg px-3"
</label> role="textbox"
<div aria-readonly="true"
id={`ajuste-dias-${index}`} >
class="bg-base-300 flex h-9 items-center rounded-lg px-3" <span class="font-bold">{diasAjustados}</span>
role="textbox" <span class="ml-2 text-xs opacity-70">dias</span>
aria-readonly="true"
>
<span class="font-bold">{periodo.diasCorridos}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/each} </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -437,7 +443,7 @@
type="button" type="button"
class="btn btn-primary btn-sm gap-2" class="btn btn-primary btn-sm gap-2"
onclick={ajustarEAprovar} onclick={ajustarEAprovar}
disabled={processando} disabled={processando || !novaDataInicio || !novaDataFim || diasAjustados <= 0}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -461,7 +467,7 @@
{/if} {/if}
<!-- Motivo Reprovação (se reprovado) --> <!-- Motivo Reprovação (se reprovado) -->
{#if solicitacao.status === 'reprovado' && solicitacao.motivoReprovacao} {#if periodo.status === 'reprovado' && periodo.motivoReprovacao}
<div class="alert alert-error mt-4"> <div class="alert alert-error mt-4">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -478,7 +484,7 @@
</svg> </svg>
<div> <div>
<div class="font-bold">Motivo da Reprovação:</div> <div class="font-bold">Motivo da Reprovação:</div>
<div class="text-sm">{solicitacao.motivoReprovacao}</div> <div class="text-sm">{periodo.motivoReprovacao}</div>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte'; import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import CalendarioFerias from './CalendarioFerias.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
@@ -30,7 +29,15 @@
let observacao = $state(''); let observacao = $state('');
let processando = $state(false); let processando = $state(false);
// Estados para os selects de data
let dataInicioPeriodo = $state('');
let dataFimPeriodo = $state('');
// Queries // Queries
const funcionarioQuery = useQuery(api.funcionarios.getById, { id: funcionarioId });
const funcionario = $derived(funcionarioQuery?.data);
const regimeTrabalho = $derived(funcionario?.regimeTrabalho || 'clt');
const saldoQuery = $derived( const saldoQuery = $derived(
useQuery(api.saldoFerias.obterSaldo, { useQuery(api.saldoFerias.obterSaldo, {
funcionarioId, funcionarioId,
@@ -62,9 +69,98 @@
return [anoAtual - 1, anoAtual, anoAtual + 1]; return [anoAtual - 1, anoAtual, anoAtual + 1];
}); });
// Configurações do calendário (baseado no saldo/regime) // Verificar se é regime estatutário PE ou Municipal
const maxPeriodos = $derived(saldo?.regimeTrabalho?.includes('Servidor') ? 2 : 3); const ehEstatutarioPEOuMunicipal = $derived(
const minDiasPorPeriodo = $derived(saldo?.regimeTrabalho?.includes('Servidor') ? 10 : 5); regimeTrabalho === 'estatutario_pe' || regimeTrabalho === 'estatutario_municipal'
);
// Função para calcular dias entre duas datas
function calcularDias(dataInicio: string, dataFim: string): number {
if (!dataInicio || !dataFim) return 0;
const inicio = new Date(dataInicio);
const fim = new Date(dataFim);
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays;
}
// Função para formatar data sem problemas de timezone
function formatarDataString(dataString: string): string {
if (!dataString) return '';
// Dividir a string da data (formato YYYY-MM-DD)
const partes = dataString.split('-');
if (partes.length !== 3) return dataString;
// Retornar no formato DD/MM/YYYY
return `${partes[2]}/${partes[1]}/${partes[0]}`;
}
// Função para adicionar período
function adicionarPeriodo() {
if (!dataInicioPeriodo || !dataFimPeriodo) {
toast.error('Selecione as datas de início e fim');
return;
}
const dias = calcularDias(dataInicioPeriodo, dataFimPeriodo);
if (dias <= 0) {
toast.error('Data de fim deve ser posterior à data de início');
return;
}
// Validações específicas para estatutário PE e Municipal
// Permite períodos fracionados: cada período deve ser 15 ou 30 dias
// Total não pode exceder 30 dias, mas pode ser menos
if (ehEstatutarioPEOuMunicipal) {
// Verificar se o período individual é válido (15 ou 30 dias)
if (dias !== 15 && dias !== 30) {
toast.error('Para seu regime, cada período deve ter exatamente 15 ou 30 dias');
return;
}
// Verificar se já tem 2 períodos
if (periodosFerias.length >= 2) {
toast.error('Máximo de 2 períodos permitidos para seu regime');
return;
}
// Verificar se o total não excede 30 dias
const novoTotal = totalDiasSelecionados + dias;
if (novoTotal > 30) {
toast.error(`O total não pode exceder 30 dias. Você já tem ${totalDiasSelecionados} dias, adicionando ${dias} dias totalizaria ${novoTotal} dias.`);
return;
}
}
// Verificar se o total não excede o saldo disponível
const novoTotal = totalDiasSelecionados + dias;
if (saldo && novoTotal > saldo.diasDisponiveis) {
toast.error(`Total de dias (${novoTotal}) excede saldo disponível (${saldo.diasDisponiveis})`);
return;
}
periodosFerias = [
...periodosFerias,
{
dataInicio: dataInicioPeriodo,
dataFim: dataFimPeriodo,
dias
}
];
toast.success(`Período de ${dias} dias adicionado! ✅`);
// Limpar campos
dataInicioPeriodo = '';
dataFimPeriodo = '';
}
// Função para remover período
function removerPeriodo(index: number) {
const removido = periodosFerias[index];
periodosFerias = periodosFerias.filter((_, i) => i !== index);
toast.info(`Período de ${removido.dias} dias removido`);
}
// Funções // Funções
function proximoPasso() { function proximoPasso() {
@@ -74,7 +170,7 @@
} }
if (passoAtual === 2 && periodosFerias.length === 0) { if (passoAtual === 2 && periodosFerias.length === 0) {
toast.error('Selecione pelo menos 1 período de férias'); toast.error('Adicione pelo menos 1 período de férias');
return; return;
} }
@@ -124,16 +220,8 @@
} }
} }
function handlePeriodoAdicionado(periodo: { dataInicio: string; dataFim: string; dias: number }) { // Calcular dias do período atual
periodosFerias = [...periodosFerias, periodo]; const diasPeriodoAtual = $derived(calcularDias(dataInicioPeriodo, dataFimPeriodo));
toast.success(`Período de ${periodo.dias} dias adicionado! ✅`);
}
function handlePeriodoRemovido(index: number) {
const removido = periodosFerias[index];
periodosFerias = periodosFerias.filter((_, i) => i !== index);
toast.info(`Período de ${removido.dias} dias removido`);
}
</script> </script>
<div class="wizard-ferias-container"> <div class="wizard-ferias-container">
@@ -216,8 +304,12 @@
<button <button
type="button" type="button"
class="btn btn-lg transition-all duration-300 hover:scale-105" class="btn btn-lg transition-all duration-300 hover:scale-105"
class:btn-primary={anoSelecionado === ano}
class:btn-outline={anoSelecionado !== ano} class:btn-outline={anoSelecionado !== ano}
style:border-color={anoSelecionado === ano ? '#f97316' : undefined}
style:border-width={anoSelecionado === ano ? '2px' : undefined}
style:color={anoSelecionado === ano ? '#000000' : undefined}
style:background-color={anoSelecionado === ano ? 'transparent' : undefined}
style:box-shadow={anoSelecionado === ano ? '0 0 10px rgba(249, 115, 22, 0.3)' : undefined}
onclick={() => (anoSelecionado = ano)} onclick={() => (anoSelecionado = ano)}
> >
{ano} {ano}
@@ -322,9 +414,14 @@
<div> <div>
<h4 class="font-bold">{saldo.regimeTrabalho}</h4> <h4 class="font-bold">{saldo.regimeTrabalho}</h4>
<p class="text-sm"> <p class="text-sm">
Período aquisitivo: {new Date(saldo.dataInicio).toLocaleDateString('pt-BR')} Período aquisitivo: {formatarDataString(saldo.dataInicio)}
a {new Date(saldo.dataFim).toLocaleDateString('pt-BR')} a {formatarDataString(saldo.dataFim)}
</p> </p>
{#if ehEstatutarioPEOuMunicipal}
<p class="mt-2 text-sm font-semibold">
⚠️ Regras: Períodos de 15 ou 30 dias. Máximo 2 períodos. Total não pode exceder 30 dias.
</p>
{/if}
</div> </div>
</div> </div>
@@ -401,18 +498,131 @@
{totalDiasSelecionados} dias | <strong>Restante:</strong> {totalDiasSelecionados} dias | <strong>Restante:</strong>
{(saldo?.diasDisponiveis || 0) - totalDiasSelecionados} dias {(saldo?.diasDisponiveis || 0) - totalDiasSelecionados} dias
</p> </p>
{#if ehEstatutarioPEOuMunicipal}
<p class="mt-2 text-sm font-semibold">
⚠️ Regras: Períodos de 15 ou 30 dias. Máximo 2 períodos. Total não pode exceder 30 dias.
</p>
{/if}
</div> </div>
</div> </div>
<!-- Calendário --> <!-- Formulário para adicionar período -->
<CalendarioFerias <div class="card bg-base-100 shadow-lg mb-6">
periodosExistentes={periodosFerias} <div class="card-body">
onPeriodoAdicionado={handlePeriodoAdicionado} <h3 class="card-title mb-4">Adicionar Período</h3>
onPeriodoRemovido={handlePeriodoRemovido}
{maxPeriodos} <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{minDiasPorPeriodo} <div class="form-control">
modoVisualizacao="month" <label class="label">
></CalendarioFerias> <span class="label-text font-semibold">Data Início</span>
</label>
<input
type="date"
class="input input-bordered"
bind:value={dataInicioPeriodo}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Data Fim</span>
</label>
<input
type="date"
class="input input-bordered"
bind:value={dataFimPeriodo}
min={dataInicioPeriodo || undefined}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Dias</span>
</label>
<div class="input input-bordered flex items-center">
<span class="font-bold text-primary">{diasPeriodoAtual}</span>
<span class="ml-2 text-sm opacity-70">dias</span>
</div>
</div>
</div>
<div class="card-actions mt-4">
<button
type="button"
class="btn btn-primary gap-2"
onclick={adicionarPeriodo}
disabled={!dataInicioPeriodo || !dataFimPeriodo || diasPeriodoAtual <= 0}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Adicionar Período
</button>
</div>
</div>
</div>
<!-- Lista de períodos adicionados -->
{#if periodosFerias.length > 0}
<div class="card bg-base-100 shadow-lg mb-6">
<div class="card-body">
<h3 class="card-title mb-4">Períodos Adicionados ({periodosFerias.length})</h3>
<div class="space-y-3">
{#each periodosFerias as periodo, index (index)}
<div class="bg-base-200 flex items-center gap-4 rounded-lg p-4">
<div
class="badge badge-lg badge-primary flex h-12 w-12 items-center justify-center font-bold text-white"
>
{index + 1}
</div>
<div class="flex-1">
<p class="font-semibold">
{formatarDataString(periodo.dataInicio)}
até
{formatarDataString(periodo.dataFim)}
</p>
<p class="text-base-content/70 text-sm">
{periodo.dias} dias corridos
</p>
</div>
<button
type="button"
class="btn btn-error btn-sm gap-2"
onclick={() => removerPeriodo(index)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Remover
</button>
</div>
{/each}
</div>
</div>
</div>
{/if}
<!-- Validações --> <!-- Validações -->
{#if validacao && periodosFerias.length > 0} {#if validacao && periodosFerias.length > 0}
@@ -529,17 +739,9 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold"> <p class="font-semibold">
{new Date(periodo.dataInicio).toLocaleDateString('pt-BR', { {formatarDataString(periodo.dataInicio)}
day: '2-digit',
month: 'long',
year: 'numeric'
})}
até até
{new Date(periodo.dataFim).toLocaleDateString('pt-BR', { {formatarDataString(periodo.dataFim)}
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</p> </p>
<p class="text-base-content/70 text-sm"> <p class="text-base-content/70 text-sm">
{periodo.dias} dias corridos {periodo.dias} dias corridos

View File

@@ -18,9 +18,7 @@
'meu-perfil' | 'minhas-ferias' | 'minhas-ausencias' | 'aprovar-ferias' | 'aprovar-ausencias' 'meu-perfil' | 'minhas-ferias' | 'minhas-ausencias' | 'aprovar-ferias' | 'aprovar-ausencias'
>('meu-perfil'); >('meu-perfil');
let solicitacaoSelecionada = $state<FunctionReturnType<typeof api.ferias.obterDetalhes> | null>( let periodoSelecionado = $state<Id<'ferias'> | null>(null);
null
);
let mostrarModalFoto = $state(false); let mostrarModalFoto = $state(false);
let uploadandoFoto = $state(false); let uploadandoFoto = $state(false);
@@ -192,12 +190,12 @@
})) }))
); );
// Estatísticas das minhas férias // Estatísticas das minhas férias (períodos individuais)
const statsMinhasFerias = $derived({ const statsMinhasFerias = $derived({
total: minhasSolicitacoes.length, total: minhasSolicitacoes.length,
aguardando: minhasSolicitacoes.filter((s) => s.status === 'aguardando_aprovacao').length, aguardando: minhasSolicitacoes.filter((s) => s.status === 'aguardando_aprovacao').length,
aprovadas: minhasSolicitacoes.filter( aprovadas: minhasSolicitacoes.filter(
(s) => s.status === 'aprovado' || s.status === 'data_ajustada_aprovada' (s) => s.status === 'aprovado' || s.status === 'data_ajustada_aprovada' || s.status === 'EmFérias'
).length, ).length,
reprovadas: minhasSolicitacoes.filter((s) => s.status === 'reprovado').length, reprovadas: minhasSolicitacoes.filter((s) => s.status === 'reprovado').length,
emFerias: funcionario?.statusFerias === 'em_ferias' ? 1 : 0 emFerias: funcionario?.statusFerias === 'em_ferias' ? 1 : 0
@@ -212,14 +210,21 @@
}); });
async function recarregar() { async function recarregar() {
solicitacaoSelecionada = null; periodoSelecionado = null;
} }
async function selecionarSolicitacao(solicitacaoId: string) { async function selecionarPeriodo(feriasId: Id<'ferias'>) {
const detalhes = await client.query(api.ferias.obterDetalhes, { periodoSelecionado = feriasId;
solicitacaoId: solicitacaoId as Id<'solicitacoesFerias'> }
});
solicitacaoSelecionada = detalhes; // Função para formatar data sem problemas de timezone
function formatarDataString(dataString: string): string {
if (!dataString) return '';
// Dividir a string da data (formato YYYY-MM-DD)
const partes = dataString.split('-');
if (partes.length !== 3) return dataString;
// Retornar no formato DD/MM/YYYY
return `${partes[2]}/${partes[1]}/${partes[0]}`;
} }
function getStatusBadge(status: string) { function getStatusBadge(status: string) {
@@ -227,7 +232,8 @@
aguardando_aprovacao: 'badge-warning', aguardando_aprovacao: 'badge-warning',
aprovado: 'badge-success', aprovado: 'badge-success',
reprovado: 'badge-error', reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info' data_ajustada_aprovada: 'badge-info',
EmFérias: 'badge-info'
}; };
return badges[status] || 'badge-neutral'; return badges[status] || 'badge-neutral';
} }
@@ -237,7 +243,8 @@
aguardando_aprovacao: 'Aguardando', aguardando_aprovacao: 'Aguardando',
aprovado: 'Aprovado', aprovado: 'Aprovado',
reprovado: 'Reprovado', reprovado: 'Reprovado',
data_ajustada_aprovada: 'Ajustado' data_ajustada_aprovada: 'Ajustado',
EmFérias: 'Em Férias'
}; };
return textos[status] || status; return textos[status] || status;
} }
@@ -1335,6 +1342,7 @@
<option value="aprovado">Aprovado</option> <option value="aprovado">Aprovado</option>
<option value="reprovado">Reprovado</option> <option value="reprovado">Reprovado</option>
<option value="data_ajustada_aprovada">Data Ajustada</option> <option value="data_ajustada_aprovada">Data Ajustada</option>
<option value="EmFérias">Em Férias</option>
</select> </select>
</div> </div>
</div> </div>
@@ -1371,27 +1379,27 @@
<thead> <thead>
<tr> <tr>
<th>Ano</th> <th>Ano</th>
<th>Períodos</th> <th>Período</th>
<th>Total Dias</th> <th>Dias</th>
<th>Status</th> <th>Status</th>
<th>Solicitado em</th> <th>Solicitado em</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each solicitacoesFiltradas as solicitacao (solicitacao._id)} {#each solicitacoesFiltradas as periodo (periodo._id)}
<tr> <tr>
<td>{solicitacao.anoReferencia}</td> <td>{periodo.anoReferencia}</td>
<td>{solicitacao.periodos.length} período(s)</td>
<td class="font-bold"
>{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)} dias</td
>
<td> <td>
<div class={`badge ${getStatusBadge(solicitacao.status)}`}> {formatarDataString(periodo.dataInicio)} - {formatarDataString(periodo.dataFim)}
{getStatusTexto(solicitacao.status)} </td>
<td class="font-bold">{periodo.diasFerias} dias</td>
<td>
<div class={`badge ${getStatusBadge(periodo.status)}`}>
{getStatusTexto(periodo.status)}
</div> </div>
</td> </td>
<td class="text-xs" <td class="text-xs"
>{new Date(solicitacao._creationTime).toLocaleDateString('pt-BR')}</td >{new Date(periodo._creationTime).toLocaleDateString('pt-BR')}</td
> >
</tr> </tr>
{/each} {/each}
@@ -1741,50 +1749,50 @@
<th class="font-bold">Funcionário</th> <th class="font-bold">Funcionário</th>
<th class="font-bold">Time</th> <th class="font-bold">Time</th>
<th class="font-bold">Ano</th> <th class="font-bold">Ano</th>
<th class="font-bold">Períodos</th> <th class="font-bold">Período</th>
<th class="font-bold">Dias</th> <th class="font-bold">Dias</th>
<th class="font-bold">Status</th> <th class="font-bold">Status</th>
<th class="font-bold">Ações</th> <th class="font-bold">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each solicitacoesSubordinados as solicitacao (solicitacao._id)} {#each solicitacoesSubordinados as periodo (periodo._id)}
<tr class="hover:bg-base-200 transition-colors"> <tr class="hover:bg-base-200 transition-colors">
<td> <td>
<div class="font-bold"> <div class="font-bold">
{solicitacao.funcionario?.nome} {periodo.funcionario?.nome}
</div> </div>
</td> </td>
<td> <td>
{#if solicitacao.time} {#if periodo.time}
<div <div
class="badge badge-lg font-semibold" class="badge badge-lg font-semibold"
style="background-color: {solicitacao.time style="background-color: {periodo.time
.cor}20; border-color: {solicitacao.time.cor}; color: {solicitacao .cor}20; border-color: {periodo.time.cor}; color: {periodo.time
.time.cor}" .cor}"
> >
{solicitacao.time.nome} {periodo.time.nome}
</div> </div>
{/if} {/if}
</td> </td>
<td class="font-semibold">{solicitacao.anoReferencia}</td> <td class="font-semibold">{periodo.anoReferencia}</td>
<td class="font-semibold">{solicitacao.periodos.length}</td> <td class="font-semibold">
<td class="text-lg font-bold" {formatarDataString(periodo.dataInicio)} - {formatarDataString(periodo.dataFim)}
>{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)}</td </td>
> <td class="text-lg font-bold">{periodo.diasFerias}</td>
<td> <td>
<div <div
class={`badge badge-lg font-semibold ${getStatusBadge(solicitacao.status)}`} class={`badge badge-lg font-semibold ${getStatusBadge(periodo.status)}`}
> >
{getStatusTexto(solicitacao.status)} {getStatusTexto(periodo.status)}
</div> </div>
</td> </td>
<td> <td>
{#if solicitacao.status === 'aguardando_aprovacao'} {#if periodo.status === 'aguardando_aprovacao'}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm gap-2 shadow-lg transition-transform hover:scale-105" class="btn btn-primary btn-sm gap-2 shadow-lg transition-transform hover:scale-105"
onclick={() => selecionarSolicitacao(solicitacao._id)} onclick={() => selecionarPeriodo(periodo._id)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1806,7 +1814,7 @@
<button <button
type="button" type="button"
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
onclick={() => selecionarSolicitacao(solicitacao._id)} onclick={() => selecionarPeriodo(periodo._id)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1997,26 +2005,28 @@
</div> </div>
<!-- Modal de Aprovação de Férias --> <!-- Modal de Aprovação de Férias -->
{#if solicitacaoSelecionada} {#if periodoSelecionado && currentUser.data}
<dialog class="modal modal-open"> {#await client.query(api.ferias.obterDetalhes, { feriasId: periodoSelecionado }) then detalhes}
<div class="modal-box max-w-4xl"> {#if detalhes}
{#if currentUser.data} <dialog class="modal modal-open">
<AprovarFerias <div class="modal-box max-w-4xl">
solicitacao={solicitacaoSelecionada} <AprovarFerias
gestorId={currentUser.data._id} periodo={detalhes}
onSucesso={recarregar} gestorId={currentUser.data._id}
onCancelar={() => (solicitacaoSelecionada = null)} onSucesso={recarregar}
/> onCancelar={() => (periodoSelecionado = null)}
{/if} />
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop">
<button <button
type="button" type="button"
onclick={() => (solicitacaoSelecionada = null)} onclick={() => (periodoSelecionado = null)}
aria-label="Fechar modal">Fechar</button aria-label="Fechar modal">Fechar</button
> >
</form> </form>
</dialog> </dialog>
{/if}
{/await}
{/if} {/if}
<!-- Modal de Upload de Foto / Escolher Avatar --> <!-- Modal de Upload de Foto / Escolher Avatar -->

View File

@@ -15,13 +15,13 @@
type TodasSolicitacoes = FunctionReturnType<typeof api.ferias.listarTodas>; type TodasSolicitacoes = FunctionReturnType<typeof api.ferias.listarTodas>;
type Solicitacao = TodasSolicitacoes[number]; type Solicitacao = TodasSolicitacoes[number];
type PeriodoDetalhado = { type PeriodoDetalhado = {
solicitacaoId: Id<'solicitacoesFerias'>; funcionarioId: Id<'funcionarios'>;
funcionarioId: Id<'funcionarios'> | null; anoReferencia: number;
feriasId: Id<'ferias'>;
funcionarioNome: string; funcionarioNome: string;
matricula?: string | null; matricula?: string | null;
timeNome?: string | null; timeNome?: string | null;
timeCor?: string | null; timeCor?: string | null;
anoReferencia: number;
status: Solicitacao['status']; status: Solicitacao['status'];
dataInicio: string; dataInicio: string;
dataFim: string; dataFim: string;
@@ -39,8 +39,8 @@
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
// Estado para controlar qual solicitação está selecionada para mudança de status // Estado para controlar qual período está selecionado para mudança de status
let solicitacaoSelecionada = $state<Id<'solicitacoesFerias'> | null>(null); let periodoSelecionado = $state<Id<'ferias'> | null>(null);
// Estados de loading e error // Estados de loading e error
const isLoading = $derived(todasSolicitacoesQuery?.isLoading ?? true); const isLoading = $derived(todasSolicitacoesQuery?.isLoading ?? true);
@@ -106,10 +106,10 @@
let dataInicioRelatorio = $state<string>(''); let dataInicioRelatorio = $state<string>('');
let dataFimRelatorio = $state<string>(''); let dataFimRelatorio = $state<string>('');
// Filtrar solicitações // Filtrar períodos individuais
const solicitacoesFiltradas = $derived( const solicitacoesFiltradas = $derived(
solicitacoes.filter((solicitacao) => { solicitacoes.filter((periodo) => {
if (filtroStatus !== 'todos' && solicitacao.status !== filtroStatus) { if (filtroStatus !== 'todos' && periodo.status !== filtroStatus) {
return false; return false;
} }
@@ -118,7 +118,7 @@
const emailFiltro = normalizarTexto(filtroEmail.trim()); const emailFiltro = normalizarTexto(filtroEmail.trim());
if (nomeFiltro || matriculaFiltro || emailFiltro) { if (nomeFiltro || matriculaFiltro || emailFiltro) {
const funcionario = solicitacao.funcionario; const funcionario = periodo.funcionario;
if (!funcionario) return false; if (!funcionario) return false;
const contato = funcionario as { const contato = funcionario as {
@@ -165,27 +165,25 @@
const inicioComparacao = inicioFiltro ?? new SvelteDate(-8640000000000000); const inicioComparacao = inicioFiltro ?? new SvelteDate(-8640000000000000);
const fimComparacao = fimFiltro ?? new SvelteDate(8640000000000000); const fimComparacao = fimFiltro ?? new SvelteDate(8640000000000000);
return solicitacao.periodos.some((periodo) => { const inicioPeriodo = criarDataHora(periodo.dataInicio, 'inicio');
const inicioPeriodo = criarDataHora(periodo.dataInicio, 'inicio'); const fimPeriodo = criarDataHora(periodo.dataFim, 'fim');
const fimPeriodo = criarDataHora(periodo.dataFim, 'fim'); if (!inicioPeriodo || !fimPeriodo) {
if (!inicioPeriodo || !fimPeriodo) { return false;
}
if (intervaloMes) {
if (fimPeriodo < intervaloMes.inicio || inicioPeriodo > intervaloMes.fim) {
return false; return false;
} }
}
if (intervaloMes) { if (inicioFiltro || fimFiltro) {
if (fimPeriodo < intervaloMes.inicio || inicioPeriodo > intervaloMes.fim) { if (fimPeriodo < inicioComparacao || inicioPeriodo > fimComparacao) {
return false; return false;
}
} }
}
if (inicioFiltro || fimFiltro) { return true;
if (fimPeriodo < inicioComparacao || inicioPeriodo > fimComparacao) {
return false;
}
}
return true;
});
}) })
); );
@@ -203,27 +201,26 @@
const solicitacoesAprovadas = $derived( const solicitacoesAprovadas = $derived(
solicitacoesFiltradas.filter( solicitacoesFiltradas.filter(
(s) => s.status === 'aprovado' || s.status === 'data_ajustada_aprovada' (p) =>
p.status === 'aprovado' || p.status === 'data_ajustada_aprovada' || p.status === 'EmFérias'
) )
); );
const periodosDetalhados = $derived<Array<PeriodoDetalhado>>( const periodosDetalhados = $derived<Array<PeriodoDetalhado>>(
solicitacoesAprovadas solicitacoesAprovadas
.flatMap((solicitacao) => .map((periodo) => ({
solicitacao.periodos.map((periodo) => ({ feriasId: periodo._id,
solicitacaoId: solicitacao._id, funcionarioId: periodo.funcionarioId,
funcionarioId: solicitacao.funcionarioId ?? null, anoReferencia: periodo.anoReferencia,
funcionarioNome: solicitacao.funcionario?.nome ?? 'Funcionário não encontrado', funcionarioNome: periodo.funcionario?.nome ?? 'Funcionário não encontrado',
matricula: solicitacao.funcionario?.matricula ?? null, matricula: periodo.funcionario?.matricula ?? null,
timeNome: solicitacao.time?.nome ?? null, timeNome: periodo.time?.nome ?? null,
timeCor: solicitacao.time?.cor ?? null, timeCor: periodo.time?.cor ?? null,
anoReferencia: solicitacao.anoReferencia, status: periodo.status,
status: solicitacao.status, dataInicio: periodo.dataInicio,
dataInicio: periodo.dataInicio, dataFim: periodo.dataFim,
dataFim: periodo.dataFim, diasCorridos: periodo.diasFerias
diasCorridos: periodo.diasCorridos }))
}))
)
.sort( .sort(
(a, b) => new SvelteDate(a.dataInicio).getTime() - new SvelteDate(b.dataInicio).getTime() (a, b) => new SvelteDate(a.dataInicio).getTime() - new SvelteDate(b.dataInicio).getTime()
) )
@@ -308,19 +305,16 @@
(() => { (() => {
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>(); const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
for (const solicitacao of solicitacoesAprovadas) { for (const periodo of solicitacoesAprovadas) {
const totalDias = solicitacao.periodos.reduce( const totalDias = periodo.diasFerias;
(acc, periodo) => acc + periodo.diasCorridos, const existente = agregados.get(periodo.anoReferencia) ?? {
0 ano: periodo.anoReferencia,
);
const existente = agregados.get(solicitacao.anoReferencia) ?? {
ano: solicitacao.anoReferencia,
solicitacoes: 0, solicitacoes: 0,
diasTotais: 0 diasTotais: 0
}; };
existente.solicitacoes += 1; existente.solicitacoes += 1;
existente.diasTotais += totalDias; existente.diasTotais += totalDias;
agregados.set(solicitacao.anoReferencia, existente); agregados.set(periodo.anoReferencia, existente);
} }
return Array.from(agregados.values()).sort((a, b) => a.ano - b.ano); return Array.from(agregados.values()).sort((a, b) => a.ano - b.ano);
@@ -441,7 +435,7 @@
periodosDetalhados.map((periodo, indice) => { periodosDetalhados.map((periodo, indice) => {
const corBase = periodo.timeCor ?? coresCalendario[indice % coresCalendario.length]; const corBase = periodo.timeCor ?? coresCalendario[indice % coresCalendario.length];
return { return {
id: `${String(periodo.solicitacaoId)}-${indice}`, id: `${String(periodo.feriasId)}-${indice}`,
title: `${periodo.funcionarioNome} (${periodo.diasCorridos} dia${periodo.diasCorridos === 1 ? '' : 's'})`, title: `${periodo.funcionarioNome} (${periodo.diasCorridos} dia${periodo.diasCorridos === 1 ? '' : 's'})`,
start: periodo.dataInicio, start: periodo.dataInicio,
end: adicionarDias(periodo.dataFim, 1), end: adicionarDias(periodo.dataFim, 1),
@@ -464,9 +458,18 @@
console.log('📅 [Eventos] Total de eventos:', eventosFerias.length); console.log('📅 [Eventos] Total de eventos:', eventosFerias.length);
console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length); console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length);
console.log('✅ [Aprovadas] Total de solicitações aprovadas:', solicitacoesAprovadas.length); console.log('✅ [Aprovadas] Total de solicitações aprovadas:', solicitacoesAprovadas.length);
console.log('📊 [PeriodosPorMes] Total:', periodosPorMes.length);
console.log('📊 [PeriodosPorMesAtivos] Total:', periodosPorMesAtivos.length);
console.log('📊 [SolicitacoesPorAno] Total:', solicitacoesPorAno.length);
if (eventosFerias.length > 0) { if (eventosFerias.length > 0) {
console.log('📅 [Eventos] Primeiro evento:', eventosFerias[0]); console.log('📅 [Eventos] Primeiro evento:', eventosFerias[0]);
} }
if (periodosPorMes.length > 0) {
console.log('📊 [PeriodosPorMes] Primeiro:', periodosPorMes[0]);
}
if (solicitacoesPorAno.length > 0) {
console.log('📊 [SolicitacoesPorAno] Primeiro:', solicitacoesPorAno[0]);
}
}); });
let calendarioContainer: HTMLDivElement | null = null; let calendarioContainer: HTMLDivElement | null = null;
@@ -692,6 +695,16 @@
return textos[status] || status; return textos[status] || status;
} }
// Função para formatar data sem problemas de timezone
function formatarDataString(dataString: string): string {
if (!dataString) return '';
// Dividir a string da data (formato YYYY-MM-DD)
const partes = dataString.split('-');
if (partes.length !== 3) return dataString;
// Retornar no formato DD/MM/YYYY
return `${partes[2]}/${partes[1]}/${partes[0]}`;
}
function formatarData(data: string | number | Date) { function formatarData(data: string | number | Date) {
const instancia = data instanceof Date ? data : new SvelteDate(data); const instancia = data instanceof Date ? data : new SvelteDate(data);
return instancia.toLocaleDateString('pt-BR'); return instancia.toLocaleDateString('pt-BR');
@@ -771,16 +784,12 @@
filtroPeriodoFim = ''; filtroPeriodoFim = '';
} }
function totalDiasSolicitacao(solicitacao: Solicitacao): number { async function selecionarPeriodo(feriasId: Id<'ferias'>) {
return solicitacao.periodos.reduce((acc, periodo) => acc + periodo.diasCorridos, 0); periodoSelecionado = feriasId;
}
async function selecionarSolicitacao(solicitacaoId: Id<'solicitacoesFerias'>) {
solicitacaoSelecionada = solicitacaoId;
} }
async function recarregar() { async function recarregar() {
solicitacaoSelecionada = null; periodoSelecionado = null;
} }
let chartContainer: HTMLDivElement | null = null; let chartContainer: HTMLDivElement | null = null;
@@ -1678,61 +1687,60 @@
<tr class="text-base-content/70 text-sm tracking-wide uppercase"> <tr class="text-base-content/70 text-sm tracking-wide uppercase">
<th>Funcionário</th> <th>Funcionário</th>
<th>Time</th> <th>Time</th>
<th>Ano</th> <th>Ano</th>
<th>Períodos</th> <th>Período</th>
<th>Dias</th> <th>Dias</th>
<th>Status</th> <th>Status</th>
<th>Solicitado em</th> <th>Solicitado em</th>
<th>Ações</th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each solicitacoesFiltradas as periodo (periodo._id)} {#each solicitacoesFiltradas as periodo (periodo._id)}
<tr> <tr>
<td> <td>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="avatar placeholder"> <div class="avatar placeholder">
<div class="bg-primary text-primary-content w-10 rounded-full"> <div class="bg-primary text-primary-content w-10 rounded-full">
<span class="text-xs" <span class="text-xs"
>{periodo.funcionario?.nome.substring(0, 2).toUpperCase()}</span >{periodo.funcionario?.nome.substring(0, 2).toUpperCase()}</span
> >
</div> </div>
</div> </div>
<div> <div>
<div class="font-bold">{periodo.funcionario?.nome}</div> <div class="font-bold">{periodo.funcionario?.nome}</div>
<div class="text-xs opacity-50"> <div class="text-xs opacity-50">
{periodo.funcionario?.matricula || 'S/N'} {periodo.funcionario?.matricula || 'S/N'}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td> <td>
{#if solicitacao.time} {#if periodo.time}
<div <div class="badge badge-outline" style="border-color: {periodo.time.cor}">
class="badge badge-outline"
style="border-color: {solicitacao.time.cor}"
>
{periodo.time.nome} {periodo.time.nome}
</div> </div>
{:else} {:else}
<span class="text-base-content/50 text-xs">Sem time</span> <span class="text-base-content/50 text-xs">Sem time</span>
{/if} {/if}
</td> </td>
<td>{solicitacao.anoReferencia}</td>
<td>{solicitacao.periodos.length} período(s)</td>
<td class="text-base-content font-bold"
>{totalDiasSolicitacao(solicitacao)} dia(s)</td
<td>{periodo.anoReferencia}</td> <td>{periodo.anoReferencia}</td>
<td> <td>
<div class={`badge ${getStatusBadge(solicitacao.status)}`}> {formatarDataString(periodo.dataInicio)} - {formatarDataString(
periodo.dataFim
)}
</td>
<td class="text-base-content font-bold">{periodo.diasFerias} dia(s)</td>
<td>
<div class={`badge ${getStatusBadge(periodo.status)}`}>
{getStatusTexto(periodo.status)} {getStatusTexto(periodo.status)}
</div> </div>
</td> </td>
<td class="text-xs">{formatarData(periodo._creationTime)}</td> <td class="text-xs">{formatarData(periodo._creationTime)}</td>
<td> <td>
<button <button
type="button" type="button"
class="btn btn-primary btn-sm gap-2" class="btn btn-primary btn-sm gap-2"
onclick={() => selecionarPeriodo(periodo._id)} onclick={() => selecionarPeriodo(periodo._id)}
> >
<svg <svg
@@ -1917,8 +1925,8 @@
{/if} {/if}
</main> </main>
<!-- Modal de Mudança de Status --> <!-- Modal de Mudança de Status -->
{#if solicitacaoSelecionada && currentUser.data} {#if periodoSelecionado && currentUser.data}
{#await client.query(api.ferias.obterDetalhes, { feriasId: periodoSelecionado }) then detalhes} {#await client.query(api.ferias.obterDetalhes, { feriasId: periodoSelecionado }) then detalhes}
{#if detalhes} {#if detalhes}
<dialog class="modal modal-open"> <dialog class="modal modal-open">
@@ -1926,13 +1934,13 @@
<AlterarStatusFerias <AlterarStatusFerias
solicitacao={detalhes} solicitacao={detalhes}
usuarioId={currentUser.data._id} usuarioId={currentUser.data._id}
onSucesso={recarregar} onSucesso={recarregar}
onCancelar={() => (periodoSelecionado = null)} onCancelar={() => (periodoSelecionado = null)}
/> />
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop">
<button <button
type="button" type="button"
onclick={() => (periodoSelecionado = null)} onclick={() => (periodoSelecionado = null)}
aria-label="Fechar modal" aria-label="Fechar modal"
> >

View File

@@ -229,23 +229,20 @@ export const obterDadosGraficos = query({
// Buscar férias do período // Buscar férias do período
try { try {
const solicitacoesFerias = await ctx.db const ferias = await ctx.db
.query('solicitacoesFerias') .query('ferias')
.filter((q) => .filter((q) =>
q.or( q.or(
q.eq(q.field('status'), 'aprovado'), q.eq(q.field('status'), 'aprovado'),
q.eq(q.field('status'), 'data_ajustada_aprovada') q.eq(q.field('status'), 'data_ajustada_aprovada'),
q.eq(q.field('status'), 'EmFérias')
) )
) )
.collect(); .collect();
solicitacoesFerias.forEach((s) => { ferias.forEach((f) => {
if (s.periodos && Array.isArray(s.periodos)) { const dias = calcularDias(f.dataInicio, f.dataFim);
s.periodos.forEach((p: { dataInicio: string; dataFim: string }) => { totalDiasPorTipo.ferias += dias;
const dias = calcularDias(p.dataInicio, p.dataFim);
totalDiasPorTipo.ferias += dias;
});
}
}); });
} catch (error) { } catch (error) {
console.error('Erro ao buscar férias para gráfico:', error); console.error('Erro ao buscar férias para gráfico:', error);
@@ -619,48 +616,42 @@ export const obterEventosCalendario = query({
// Integrar com férias (se não estiver filtrando por tipo específico) // Integrar com férias (se não estiver filtrando por tipo específico)
if (!args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'ferias') { if (!args.tipoFiltro || args.tipoFiltro === 'todos' || args.tipoFiltro === 'ferias') {
try { try {
// Buscar solicitações de férias aprovadas // Buscar férias aprovadas
const solicitacoesFerias = await ctx.db const ferias = await ctx.db
.query('solicitacoesFerias') .query('ferias')
.filter((q) => .filter((q) =>
q.or( q.or(
q.eq(q.field('status'), 'aprovado'), q.eq(q.field('status'), 'aprovado'),
q.eq(q.field('status'), 'data_ajustada_aprovada') q.eq(q.field('status'), 'data_ajustada_aprovada'),
q.eq(q.field('status'), 'EmFérias')
) )
) )
.collect(); .collect();
for (const solicitacao of solicitacoesFerias) { for (const feriasRegistro of ferias) {
try { try {
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(feriasRegistro.funcionarioId);
if (!funcionario) continue; if (!funcionario) continue;
// Verificar se periodos existe e é um array if (!feriasRegistro.dataInicio || !feriasRegistro.dataFim) continue;
if (!solicitacao.periodos || !Array.isArray(solicitacao.periodos)) {
continue;
}
for (const periodo of solicitacao.periodos) { eventos.push({
if (!periodo.dataInicio || !periodo.dataFim) continue; id: `ferias-${feriasRegistro._id}`,
title: `${funcionario.nome} - Férias`,
eventos.push({ start: feriasRegistro.dataInicio,
id: `ferias-${solicitacao._id}-${periodo.dataInicio}`, end: feriasRegistro.dataFim,
title: `${funcionario.nome} - Férias`, color: '#10b981', // verde
start: periodo.dataInicio, tipo: 'ferias',
end: periodo.dataFim, funcionarioNome: funcionario.nome,
color: '#10b981', // verde funcionarioId: funcionario._id
tipo: 'ferias', });
funcionarioNome: funcionario.nome,
funcionarioId: funcionario._id
});
}
} catch (error) { } catch (error) {
console.error(`Erro ao processar solicitação de férias ${solicitacao._id}:`, error); console.error(`Erro ao processar férias ${feriasRegistro._id}:`, error);
continue; continue;
} }
} }
} catch (error) { } catch (error) {
console.error('Erro ao buscar solicitações de férias:', error); console.error('Erro ao buscar férias:', error);
// Continua mesmo se houver erro ao buscar férias // Continua mesmo se houver erro ao buscar férias
} }
} }

View File

@@ -32,13 +32,6 @@ crons.interval(
{} {}
); );
// Criar períodos aquisitivos de férias automaticamente (diariamente)
crons.interval(
"criar-periodos-aquisitivos",
{ hours: 24 },
internal.saldoFerias.criarPeriodosAquisitivos,
{}
);
export default crons; export default crons;

View File

@@ -138,9 +138,25 @@ export const enfileirarEmail = mutation({
agendadaPara: args.agendadaPara, agendadaPara: args.agendadaPara,
}); });
// O cron job processará emails automaticamente: // Processar imediatamente se não houver agendamento ou se o agendamento já passou
// - Emails sem agendamento serão processados imediatamente (próxima execução do cron) const agora = Date.now();
// - Emails agendados serão processados quando a hora chegar const deveProcessarAgora =
args.agendadaPara === undefined ||
args.agendadaPara <= agora;
if (deveProcessarAgora) {
// Agendar envio imediato via action (não bloqueia a mutation)
ctx.scheduler
.runAfter(0, api.actions.email.enviar, {
emailId: emailId,
})
.catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar envio imediato de email ${emailId}:`, errorMessage);
// Não falha a mutation se houver erro ao agendar - o cron pode processar depois
});
}
// Emails agendados para o futuro serão processados pelo cron quando a hora chegar
return emailId; return emailId;
}, },

View File

@@ -10,22 +10,81 @@ const periodoValidator = v.object({
diasCorridos: v.number(), diasCorridos: v.number(),
}); });
// Query: Listar TODAS as solicitações (para RH) // Helper: Calcular dias entre duas datas
// Retorna tipo inferido automaticamente pelo Convex function calcularDiasEntreDatas(dataInicio: string, dataFim: string): number {
const inicio = new Date(dataInicio);
const fim = new Date(dataFim);
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays;
}
// Helper: Agrupar registros de ferias por funcionarioId + anoReferencia
function agruparPorSolicitacao(
registros: Array<Doc<"ferias">>
): Array<{
funcionarioId: Id<"funcionarios">;
anoReferencia: number;
periodos: Array<Doc<"ferias">>;
status: string;
observacao?: string;
motivoReprovacao?: string;
gestorId?: Id<"usuarios">;
dataAprovacao?: number;
dataReprovacao?: number;
historicoAlteracoes?: Array<{
data: number;
usuarioId: Id<"usuarios">;
acao: string;
}>;
}> {
const grupos = new Map<string, Array<Doc<"ferias">>>();
for (const registro of registros) {
const chave = `${registro.funcionarioId}_${registro.anoReferencia}`;
if (!grupos.has(chave)) {
grupos.set(chave, []);
}
grupos.get(chave)!.push(registro);
}
return Array.from(grupos.entries()).map(([_, periodos]) => {
// Ordenar por data de criação para manter ordem
periodos.sort((a, b) => a._creationTime - b._creationTime);
// Pegar informações da primeira solicitação (todos têm os mesmos campos compartilhados)
const primeiro = periodos[0];
return {
funcionarioId: primeiro.funcionarioId,
anoReferencia: primeiro.anoReferencia,
periodos,
status: primeiro.status,
observacao: primeiro.observacao,
motivoReprovacao: primeiro.motivoReprovacao,
gestorId: primeiro.gestorId,
dataAprovacao: primeiro.dataAprovacao,
dataReprovacao: primeiro.dataReprovacao,
historicoAlteracoes: primeiro.historicoAlteracoes,
};
});
}
// Query: Listar TODAS as solicitações (para RH) - períodos individuais
export const listarTodas = query({ export const listarTodas = query({
args: {}, args: {},
handler: async (ctx) => { handler: async (ctx) => {
const solicitacoes = await ctx.db.query("solicitacoesFerias").collect(); const todasFerias = await ctx.db.query("ferias").collect();
const solicitacoesComDetalhes = await Promise.all( const periodosComDetalhes = await Promise.all(
solicitacoes.map(async (s) => { todasFerias.map(async (ferias) => {
const funcionario = await ctx.db.get(s.funcionarioId); const funcionario = await ctx.db.get(ferias.funcionarioId);
// Buscar time do funcionário // Buscar time do funcionário
const membroTime = await ctx.db const membroTime = await ctx.db
.query("timesMembros") .query("timesMembros")
.withIndex("by_funcionario", (q) => .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", s.funcionarioId) q.eq("funcionarioId", ferias.funcionarioId)
) )
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field("ativo"), true))
.first(); .first();
@@ -36,65 +95,54 @@ export const listarTodas = query({
} }
return { return {
...s, ...ferias,
funcionario, funcionario,
time, time,
}; };
}) })
); );
return solicitacoesComDetalhes.sort( return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
(a, b) => b._creationTime - a._creationTime
);
}, },
}); });
// Query: Listar solicitações do funcionário // Query: Listar solicitações do funcionário - períodos individuais
export const listarMinhasSolicitacoes = query({ export const listarMinhasSolicitacoes = query({
args: { funcionarioId: v.id("funcionarios") }, args: { funcionarioId: v.id("funcionarios") },
// returns não especificado - TypeScript inferirá automaticamente o tipo correto
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacoes = await ctx.db const todasFerias = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.withIndex("by_funcionario", (q) => .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", args.funcionarioId) q.eq("funcionarioId", args.funcionarioId)
) )
.order("desc")
.collect(); .collect();
// Enriquecer com dados do funcionário e time const funcionario = await ctx.db.get(args.funcionarioId);
const solicitacoesComDetalhes = await Promise.all(
solicitacoes.map(async (s) => {
const funcionario = await ctx.db.get(s.funcionarioId);
// Buscar time do funcionário // Buscar time do funcionário
const membroTime = await ctx.db const membroTime = await ctx.db
.query("timesMembros") .query("timesMembros")
.withIndex("by_funcionario", (q) => .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", s.funcionarioId) q.eq("funcionarioId", args.funcionarioId)
) )
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field("ativo"), true))
.first(); .first();
let time = null; let time = null;
if (membroTime) { if (membroTime) {
time = await ctx.db.get(membroTime.timeId); time = await ctx.db.get(membroTime.timeId);
} }
return { // Retornar períodos individuais com detalhes
...s, return todasFerias.map((ferias) => ({
funcionario, ...ferias,
time, funcionario,
}; time,
}) })).sort((a, b) => b._creationTime - a._creationTime);
);
return solicitacoesComDetalhes;
}, },
}); });
// Query: Listar solicitações dos subordinados (para gestores) // Query: Listar solicitações dos subordinados (para gestores) - períodos individuais
// Retorna tipo inferido automaticamente pelo Convex
export const listarSolicitacoesSubordinados = query({ export const listarSolicitacoesSubordinados = query({
args: { gestorId: v.id("usuarios") }, args: { gestorId: v.id("usuarios") },
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -105,10 +153,7 @@ export const listarSolicitacoesSubordinados = query({
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field("ativo"), true))
.collect(); .collect();
const solicitacoes: Array<Doc<"solicitacoesFerias"> & { const todasFerias: Array<Doc<"ferias">> = [];
funcionario: Doc<"funcionarios"> | null;
time: Doc<"times"> | null;
}> = [];
for (const time of timesGestor) { for (const time of timesGestor) {
// Buscar membros do time // Buscar membros do time
@@ -119,54 +164,90 @@ export const listarSolicitacoesSubordinados = query({
) )
.collect(); .collect();
// Buscar solicitações de cada membro // Buscar férias de cada membro
for (const membro of membros) { for (const membro of membros) {
const solic = await ctx.db const ferias = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.withIndex("by_funcionario", (q) => .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", membro.funcionarioId) q.eq("funcionarioId", membro.funcionarioId)
) )
.collect(); .collect();
// Adicionar info do funcionário todasFerias.push(...ferias);
for (const s of solic) {
const funcionario = await ctx.db.get(s.funcionarioId);
solicitacoes.push({
...s,
funcionario,
time,
});
}
} }
} }
return solicitacoes.sort((a, b) => b._creationTime - a._creationTime); // Adicionar info do funcionário e time para cada período
const periodosComDetalhes = await Promise.all(
todasFerias.map(async (ferias) => {
const funcionario = await ctx.db.get(ferias.funcionarioId);
// Buscar time do funcionário
const membroTime = await ctx.db
.query("timesMembros")
.withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", ferias.funcionarioId)
)
.filter((q) => q.eq(q.field("ativo"), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
}
return {
...ferias,
funcionario,
time,
};
})
);
return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
}, },
}); });
// Query: Obter detalhes completos de uma solicitação // Query: Obter detalhes de um período individual
// Retorna tipo inferido automaticamente pelo Convex
export const obterDetalhes = query({ export const obterDetalhes = query({
args: { solicitacaoId: v.id("solicitacoesFerias") }, args: {
feriasId: v.id("ferias")
},
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); const ferias = await ctx.db.get(args.feriasId);
if (!solicitacao) return null;
const funcionario = await ctx.db.get(solicitacao.funcionarioId); if (!ferias) return null;
const funcionario = await ctx.db.get(ferias.funcionarioId);
let gestor = null; let gestor = null;
if (solicitacao.gestorId) { if (ferias.gestorId) {
gestor = await ctx.db.get(solicitacao.gestorId); gestor = await ctx.db.get(ferias.gestorId);
}
// Buscar time do funcionário
const membroTime = await ctx.db
.query("timesMembros")
.withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", ferias.funcionarioId)
)
.filter((q) => q.eq(q.field("ativo"), true))
.first();
let time = null;
if (membroTime) {
time = await ctx.db.get(membroTime.timeId);
} }
return { return {
...solicitacao, ...ferias,
funcionario, funcionario,
gestor, gestor,
time,
}; };
}, },
}); });
// Mutation: Criar solicitação de férias (com validação de saldo) // Mutation: Criar solicitação de férias (cria um registro por período)
export const criarSolicitacao = mutation({ export const criarSolicitacao = mutation({
args: { args: {
funcionarioId: v.id("funcionarios"), funcionarioId: v.id("funcionarios"),
@@ -174,7 +255,7 @@ export const criarSolicitacao = mutation({
periodos: v.array(periodoValidator), periodos: v.array(periodoValidator),
observacao: v.optional(v.string()), observacao: v.optional(v.string()),
}, },
returns: v.id("solicitacoesFerias"), returns: v.array(v.id("ferias")),
handler: async (ctx, args) => { handler: async (ctx, args) => {
if (args.periodos.length === 0) { if (args.periodos.length === 0) {
throw new Error("É necessário adicionar pelo menos 1 período"); throw new Error("É necessário adicionar pelo menos 1 período");
@@ -183,19 +264,6 @@ export const criarSolicitacao = mutation({
const funcionario = await ctx.db.get(args.funcionarioId); const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) throw new Error("Funcionário não encontrado"); if (!funcionario) throw new Error("Funcionário não encontrado");
// Calcular total de dias
let totalDias = 0;
for (const p of args.periodos) {
totalDias += p.diasCorridos;
}
// Reservar dias no saldo (impede uso duplo)
await ctx.runMutation(internal.saldoFerias.reservarDias, {
funcionarioId: args.funcionarioId,
anoReferencia: args.anoReferencia,
totalDias,
});
// Buscar usuário que está criando (pode não ser o próprio funcionário) // Buscar usuário que está criando (pode não ser o próprio funcionário)
const usuario = await ctx.db const usuario = await ctx.db
.query("usuarios") .query("usuarios")
@@ -204,59 +272,75 @@ export const criarSolicitacao = mutation({
) )
.first(); .first();
const solicitacaoId = await ctx.db.insert("solicitacoesFerias", { const historicoInicial = [
funcionarioId: args.funcionarioId, {
anoReferencia: args.anoReferencia, data: Date.now(),
status: "aguardando_aprovacao", usuarioId: usuario?._id || funcionario.gestorId!,
periodos: args.periodos, acao: "Solicitação criada",
observacao: args.observacao, },
historicoAlteracoes: [ ];
{
data: Date.now(),
usuarioId: usuario?._id || funcionario.gestorId!,
acao: "Solicitação criada",
},
],
});
// Notificar gestor // Criar um registro para cada período
if (funcionario.gestorId) { const idsCriados: Array<Id<"ferias">> = [];
for (const periodo of args.periodos) {
const feriasId = await ctx.db.insert("ferias", {
funcionarioId: args.funcionarioId,
anoReferencia: args.anoReferencia,
dataInicio: periodo.dataInicio,
dataFim: periodo.dataFim,
diasFerias: periodo.diasCorridos,
status: "aguardando_aprovacao",
observacao: args.observacao,
diasAbono: 0,
historicoAlteracoes: historicoInicial,
});
idsCriados.push(feriasId);
}
// Notificar gestor (usar o primeiro ID criado)
if (funcionario.gestorId && idsCriados.length > 0) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: funcionario.gestorId, destinatarioId: funcionario.gestorId,
solicitacaoFeriasId: solicitacaoId, feriasId: idsCriados[0],
tipo: "nova_solicitacao", tipo: "nova_solicitacao",
lida: false, lida: false,
mensagem: `${funcionario.nome} solicitou férias`, mensagem: `${funcionario.nome} solicitou férias`,
}); });
} }
return solicitacaoId; return idsCriados;
}, },
}); });
// Mutation: Aprovar férias // Mutation: Aprovar período de férias individual
export const aprovar = mutation({ export const aprovar = mutation({
args: { args: {
solicitacaoId: v.id("solicitacoesFerias"), feriasId: v.id("ferias"),
gestorId: v.id("usuarios"), gestorId: v.id("usuarios"),
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); // Buscar o registro específico
if (!solicitacao) throw new Error("Solicitação não encontrada"); const registro = await ctx.db.get(args.feriasId);
if (solicitacao.status !== "aguardando_aprovacao") { if (!registro) {
throw new Error("Esta solicitação já foi processada"); throw new Error("Período de férias não encontrado");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); // Verificar se está aguardando aprovação
if (registro.status !== "aguardando_aprovacao") {
throw new Error("Este período já foi processado");
}
await ctx.db.patch(args.solicitacaoId, { const funcionario = await ctx.db.get(registro.funcionarioId);
// Atualizar o registro
await ctx.db.patch(registro._id, {
status: "aprovado", status: "aprovado",
gestorId: args.gestorId, gestorId: args.gestorId,
dataAprovacao: Date.now(), dataAprovacao: Date.now(),
historicoAlteracoes: [ historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []), ...(registro.historicoAlteracoes || []),
{ {
data: Date.now(), data: Date.now(),
usuarioId: args.gestorId, usuarioId: args.gestorId,
@@ -265,11 +349,6 @@ export const aprovar = mutation({
], ],
}); });
// Atualizar saldo (de pendente para usado)
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
solicitacaoId: args.solicitacaoId,
});
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
@@ -282,10 +361,10 @@ export const aprovar = mutation({
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
solicitacaoFeriasId: args.solicitacaoId, feriasId: registro._id,
tipo: "aprovado", tipo: "aprovado",
lida: false, lida: false,
mensagem: "Suas férias foram aprovadas!", mensagem: `Período de férias de ${registro.diasFerias} dias foi aprovado!`,
}); });
} }
} }
@@ -294,31 +373,37 @@ export const aprovar = mutation({
}, },
}); });
// Mutation: Reprovar férias // Mutation: Reprovar período de férias individual
export const reprovar = mutation({ export const reprovar = mutation({
args: { args: {
solicitacaoId: v.id("solicitacoesFerias"), feriasId: v.id("ferias"),
gestorId: v.id("usuarios"), gestorId: v.id("usuarios"),
motivoReprovacao: v.string(), motivoReprovacao: v.string(),
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); // Buscar o registro específico
if (!solicitacao) throw new Error("Solicitação não encontrada"); const registro = await ctx.db.get(args.feriasId);
if (solicitacao.status !== "aguardando_aprovacao") { if (!registro) {
throw new Error("Esta solicitação já foi processada"); throw new Error("Período de férias não encontrado");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); // Verificar se está aguardando aprovação
if (registro.status !== "aguardando_aprovacao") {
throw new Error("Este período já foi processado");
}
await ctx.db.patch(args.solicitacaoId, { const funcionario = await ctx.db.get(registro.funcionarioId);
// Atualizar o registro
await ctx.db.patch(registro._id, {
status: "reprovado", status: "reprovado",
gestorId: args.gestorId, gestorId: args.gestorId,
dataReprovacao: Date.now(), dataReprovacao: Date.now(),
motivoReprovacao: args.motivoReprovacao, motivoReprovacao: args.motivoReprovacao,
historicoAlteracoes: [ historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []), ...(registro.historicoAlteracoes || []),
{ {
data: Date.now(), data: Date.now(),
usuarioId: args.gestorId, usuarioId: args.gestorId,
@@ -327,11 +412,6 @@ export const reprovar = mutation({
], ],
}); });
// Liberar dias reservados de volta ao saldo
await ctx.runMutation(internal.saldoFerias.liberarDias, {
solicitacaoId: args.solicitacaoId,
});
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
@@ -344,10 +424,10 @@ export const reprovar = mutation({
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
solicitacaoFeriasId: args.solicitacaoId, feriasId: registro._id,
tipo: "reprovado", tipo: "reprovado",
lida: false, lida: false,
mensagem: `Suas férias foram reprovadas: ${args.motivoReprovacao}`, mensagem: `Período de férias de ${registro.diasFerias} dias foi reprovado: ${args.motivoReprovacao}`,
}); });
} }
} }
@@ -356,66 +436,51 @@ export const reprovar = mutation({
}, },
}); });
// Mutation: Ajustar data e aprovar // Mutation: Ajustar data e aprovar período individual
export const ajustarEAprovar = mutation({ export const ajustarEAprovar = mutation({
args: { args: {
solicitacaoId: v.id("solicitacoesFerias"), feriasId: v.id("ferias"),
gestorId: v.id("usuarios"), gestorId: v.id("usuarios"),
novosPeriodos: v.array(periodoValidator), novaDataInicio: v.string(),
novaDataFim: v.string(),
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); // Buscar o registro específico
if (!solicitacao) throw new Error("Solicitação não encontrada"); const registroAntigo = await ctx.db.get(args.feriasId);
if (solicitacao.status !== "aguardando_aprovacao") { if (!registroAntigo) {
throw new Error("Esta solicitação já foi processada"); throw new Error("Período de férias não encontrado");
} }
if (args.novosPeriodos.length === 0) { // Verificar se está aguardando aprovação
throw new Error("É necessário adicionar pelo menos 1 período"); if (registroAntigo.status !== "aguardando_aprovacao") {
throw new Error("Este período já foi processado");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(registroAntigo.funcionarioId);
// Liberar dias antigos // Calcular novos dias
await ctx.runMutation(internal.saldoFerias.liberarDias, { const novosDias = calcularDiasEntreDatas(args.novaDataInicio, args.novaDataFim);
solicitacaoId: args.solicitacaoId,
});
// Calcular novos dias e reservar // Atualizar o registro com novas datas
let totalNovosDias = 0; await ctx.db.patch(registroAntigo._id, {
for (const p of args.novosPeriodos) { dataInicio: args.novaDataInicio,
totalNovosDias += p.diasCorridos; dataFim: args.novaDataFim,
} diasFerias: novosDias,
await ctx.runMutation(internal.saldoFerias.reservarDias, {
funcionarioId: solicitacao.funcionarioId,
anoReferencia: solicitacao.anoReferencia,
totalDias: totalNovosDias,
});
await ctx.db.patch(args.solicitacaoId, {
status: "data_ajustada_aprovada", status: "data_ajustada_aprovada",
periodos: args.novosPeriodos,
gestorId: args.gestorId, gestorId: args.gestorId,
dataAprovacao: Date.now(), dataAprovacao: Date.now(),
historicoAlteracoes: [ historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []), ...(registroAntigo.historicoAlteracoes || []),
{ {
data: Date.now(), data: Date.now(),
usuarioId: args.gestorId, usuarioId: args.gestorId,
acao: "Data ajustada e aprovada", acao: `Data ajustada e aprovada: ${registroAntigo.dataInicio} - ${registroAntigo.dataFim}${args.novaDataInicio} - ${args.novaDataFim}`,
periodosAnteriores: solicitacao.periodos,
}, },
], ],
}); });
// Atualizar saldo (marcar como usado)
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
solicitacaoId: args.solicitacaoId,
});
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
@@ -428,10 +493,10 @@ export const ajustarEAprovar = mutation({
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
solicitacaoFeriasId: args.solicitacaoId, feriasId: registroAntigo._id,
tipo: "data_ajustada", tipo: "data_ajustada",
lida: false, lida: false,
mensagem: "Suas férias foram aprovadas com ajuste de datas", mensagem: `Período de férias foi aprovado com ajuste de datas: ${args.novaDataInicio} a ${args.novaDataFim}`,
}); });
} }
} }
@@ -448,15 +513,15 @@ export const verificarStatusFerias = query({
const hoje = new Date(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
const solicitacoesAprovadas = await ctx.db const feriasAprovadas = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", args.funcionarioId).eq("status", "aprovado") q.eq("funcionarioId", args.funcionarioId).eq("status", "aprovado")
) )
.collect(); .collect();
const solicitacoesAjustadas = await ctx.db const feriasAjustadas = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q q
.eq("funcionarioId", args.funcionarioId) .eq("funcionarioId", args.funcionarioId)
@@ -464,21 +529,27 @@ export const verificarStatusFerias = query({
) )
.collect(); .collect();
const todasSolicitacoes = [ const feriasEmFerias = await ctx.db
...solicitacoesAprovadas, .query("ferias")
...solicitacoesAjustadas, .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", args.funcionarioId).eq("status", "EmFérias")
)
.collect();
const todasFerias = [
...feriasAprovadas,
...feriasAjustadas,
...feriasEmFerias,
]; ];
for (const solicitacao of todasSolicitacoes) { for (const ferias of todasFerias) {
for (const periodo of solicitacao.periodos) { const inicio = new Date(ferias.dataInicio);
const inicio = new Date(periodo.dataInicio); const fim = new Date(ferias.dataFim);
const fim = new Date(periodo.dataFim); inicio.setHours(0, 0, 0, 0);
inicio.setHours(0, 0, 0, 0); fim.setHours(23, 59, 59, 999);
fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) { if (hoje >= inicio && hoje <= fim) {
return "em_ferias"; return "em_ferias";
}
} }
} }
@@ -509,10 +580,10 @@ export const marcarComoLida = mutation({
}, },
}); });
// Mutation: Atualizar status da solicitação (para voltar para aguardando_aprovacao) // Mutation: Atualizar status de um período individual
export const atualizarStatus = mutation({ export const atualizarStatus = mutation({
args: { args: {
solicitacaoId: v.id("solicitacoesFerias"), feriasId: v.id("ferias"),
novoStatus: v.union( novoStatus: v.union(
v.literal("aguardando_aprovacao"), v.literal("aguardando_aprovacao"),
v.literal("aprovado"), v.literal("aprovado"),
@@ -523,40 +594,16 @@ export const atualizarStatus = mutation({
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); // Buscar o registro específico
if (!solicitacao) throw new Error("Solicitação não encontrada"); const registro = await ctx.db.get(args.feriasId);
// Se está mudando de aprovado para aguardando_aprovacao, precisa liberar os dias if (!registro) {
if (solicitacao.status === "aprovado" || solicitacao.status === "data_ajustada_aprovada") { throw new Error("Período de férias não encontrado");
if (args.novoStatus === "aguardando_aprovacao") {
// Liberar dias de volta ao saldo
await ctx.runMutation(internal.saldoFerias.liberarDias, {
solicitacaoId: args.solicitacaoId,
});
}
}
// Se está mudando de reprovado para aguardando_aprovacao, os dias já foram liberados anteriormente
// Mas precisamos reservar novamente
if (solicitacao.status === "reprovado" && args.novoStatus === "aguardando_aprovacao") {
// Calcular total de dias
let totalDias = 0;
for (const p of solicitacao.periodos) {
totalDias += p.diasCorridos;
}
// Reservar dias novamente
await ctx.runMutation(internal.saldoFerias.reservarDias, {
funcionarioId: solicitacao.funcionarioId,
anoReferencia: solicitacao.anoReferencia,
totalDias,
});
} }
// Atualizar status e histórico // Atualizar status e histórico
const acao = `Status alterado de ${solicitacao.status} para ${args.novoStatus}`; const acao = `Status alterado para ${args.novoStatus}`;
// Preparar dados de atualização
const updateData: { const updateData: {
status: typeof args.novoStatus; status: typeof args.novoStatus;
historicoAlteracoes: Array<{ historicoAlteracoes: Array<{
@@ -564,10 +611,14 @@ export const atualizarStatus = mutation({
usuarioId: Id<"usuarios">; usuarioId: Id<"usuarios">;
acao: string; acao: string;
}>; }>;
gestorId?: undefined;
dataAprovacao?: undefined;
dataReprovacao?: undefined;
motivoReprovacao?: undefined;
} = { } = {
status: args.novoStatus, status: args.novoStatus,
historicoAlteracoes: [ historicoAlteracoes: [
...(solicitacao.historicoAlteracoes || []), ...(registro.historicoAlteracoes || []),
{ {
data: Date.now(), data: Date.now(),
usuarioId: args.usuarioId, usuarioId: args.usuarioId,
@@ -576,16 +627,17 @@ export const atualizarStatus = mutation({
], ],
}; };
// Se voltar para aguardando_aprovacao, limpar campos relacionados usando replace // Se voltar para aguardando_aprovacao, limpar campos relacionados
if (args.novoStatus === "aguardando_aprovacao") { if (args.novoStatus === "aguardando_aprovacao") {
// Usar replace para limpar campos opcionais - omitir os campos que queremos limpar await ctx.db.patch(registro._id, {
const { gestorId, dataAprovacao, dataReprovacao, motivoReprovacao, ...solicitacaoLimpa } = solicitacao;
await ctx.db.replace(args.solicitacaoId, {
...solicitacaoLimpa,
...updateData, ...updateData,
gestorId: undefined,
dataAprovacao: undefined,
dataReprovacao: undefined,
motivoReprovacao: undefined,
}); });
} else { } else {
await ctx.db.patch(args.solicitacaoId, updateData); await ctx.db.patch(registro._id, updateData);
} }
return null; return null;
@@ -603,39 +655,106 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
const hoje = new Date(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
const solicitacoesAprovadas = await ctx.db // Buscar todos os registros de férias que podem estar em férias
.query("solicitacoesFerias") // Buscar por status específico para criar mapas de referência
const feriasAprovadas = await ctx.db
.query("ferias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", func._id).eq("status", "aprovado") q.eq("funcionarioId", func._id).eq("status", "aprovado")
) )
.collect(); .collect();
const solicitacoesAjustadas = await ctx.db const feriasAjustadas = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", func._id).eq("status", "data_ajustada_aprovada") q.eq("funcionarioId", func._id).eq("status", "data_ajustada_aprovada")
) )
.collect(); .collect();
const todasSolicitacoes = [ const feriasEmFerias = await ctx.db
...solicitacoesAprovadas, .query("ferias")
...solicitacoesAjustadas, .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", func._id).eq("status", "EmFérias")
)
.collect();
// Criar mapas para verificar status original
// Quando um registro está "EmFérias", precisamos saber qual era o status anterior
// Vamos usar o histórico ou verificar se o ID estava nas listas antes
const idsAprovados = new Set(feriasAprovadas.map(f => f._id));
const idsAjustados = new Set(feriasAjustadas.map(f => f._id));
// Para registros que estão "EmFérias", verificar o histórico para determinar status anterior
// Se não houver histórico claro, usar lógica: se foi aprovado recentemente, provavelmente era "aprovado"
// Por enquanto, vamos usar uma heurística: se o registro está "EmFérias" e não está nas listas,
// vamos verificar o histórico de alterações para encontrar o status anterior
const statusAnteriorPorId = new Map<Id<"ferias">, "aprovado" | "data_ajustada_aprovada">();
for (const ferias of feriasEmFerias) {
// Verificar histórico para encontrar status anterior
if (ferias.historicoAlteracoes && ferias.historicoAlteracoes.length > 0) {
// Procurar pela última alteração que mudou para "EmFérias" ou antes disso
const historico = ferias.historicoAlteracoes;
for (let i = historico.length - 1; i >= 0; i--) {
const entrada = historico[i];
if (entrada.acao.includes("Aprovado") || entrada.acao.includes("aprovado")) {
statusAnteriorPorId.set(ferias._id, "aprovado");
break;
} else if (entrada.acao.includes("Data ajustada") || entrada.acao.includes("ajustada")) {
statusAnteriorPorId.set(ferias._id, "data_ajustada_aprovada");
break;
}
}
}
// Se não encontrou no histórico, usar fallback: assumir "aprovado"
if (!statusAnteriorPorId.has(ferias._id)) {
statusAnteriorPorId.set(ferias._id, "aprovado");
}
}
// Combinar todos os registros
const todasFerias = [
...feriasAprovadas,
...feriasAjustadas,
...feriasEmFerias,
]; ];
let emFerias = false; let emFerias = false;
for (const solicitacao of todasSolicitacoes) { for (const ferias of todasFerias) {
for (const periodo of solicitacao.periodos) { const inicio = new Date(ferias.dataInicio);
const inicio = new Date(periodo.dataInicio); const fim = new Date(ferias.dataFim);
const fim = new Date(periodo.dataFim); inicio.setHours(0, 0, 0, 0);
inicio.setHours(0, 0, 0, 0); fim.setHours(23, 59, 59, 999);
fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) { if (hoje >= inicio && hoje <= fim) {
emFerias = true; emFerias = true;
break;
// Atualizar status para "EmFérias" se ainda não estiver
if (ferias.status !== "EmFérias") {
await ctx.db.patch(ferias._id, {
status: "EmFérias",
});
}
} else {
// Se saiu do período e está "EmFérias", voltar para o status anterior
if (ferias.status === "EmFérias") {
// Determinar status anterior
let statusAnterior: "aprovado" | "data_ajustada_aprovada";
if (idsAprovados.has(ferias._id)) {
statusAnterior = "aprovado";
} else if (idsAjustados.has(ferias._id)) {
statusAnterior = "data_ajustada_aprovada";
} else {
// Usar histórico ou fallback
statusAnterior = statusAnteriorPorId.get(ferias._id) || "aprovado";
}
await ctx.db.patch(ferias._id, {
status: statusAnterior,
});
} }
} }
if (emFerias) break;
} }
const novoStatus = emFerias ? "em_ferias" : "ativo"; const novoStatus = emFerias ? "em_ferias" : "ativo";

View File

@@ -1,6 +1,5 @@
import { v } from "convex/values"; import { v } from "convex/values";
import { query, mutation, internalMutation } from "./_generated/server"; import { query } from "./_generated/server";
import { internal } from "./_generated/api";
import { Id } from "./_generated/dataModel"; import { Id } from "./_generated/dataModel";
import type { QueryCtx } from "./_generated/server"; import type { QueryCtx } from "./_generated/server";
@@ -53,10 +52,11 @@ const REGIMES_CONFIG = {
estatutario_pe: { estatutario_pe: {
nome: "Servidor Público Estadual de Pernambuco", nome: "Servidor Público Estadual de Pernambuco",
maxPeriodos: 2, maxPeriodos: 2,
minDiasPeriodo: 10, minDiasPeriodo: 15, // Mínimo 15 dias por período
minDiasPeriodoPrincipal: null, // Não há essa regra minDiasPeriodoPrincipal: null, // Não há essa regra
abonoPermitido: false, abonoPermitido: false,
maxDiasAbono: 0, maxDiasAbono: 0,
periodosPermitidos: [15, 30], // Apenas 15 ou 30 dias por período
}, },
estatutario_federal: { estatutario_federal: {
nome: "Servidor Público Federal", nome: "Servidor Público Federal",
@@ -69,10 +69,11 @@ const REGIMES_CONFIG = {
estatutario_municipal: { estatutario_municipal: {
nome: "Servidor Público Municipal", nome: "Servidor Público Municipal",
maxPeriodos: 2, maxPeriodos: 2,
minDiasPeriodo: 10, minDiasPeriodo: 15, // Mínimo 15 dias por período
minDiasPeriodoPrincipal: null, minDiasPeriodoPrincipal: null,
abonoPermitido: false, abonoPermitido: false,
maxDiasAbono: 0, maxDiasAbono: 0,
periodosPermitidos: [15, 30], // Apenas 15 ou 30 dias por período
}, },
}; };
@@ -98,6 +99,103 @@ async function obterRegimeTrabalho(ctx: QueryCtx, funcionarioId: Id<"funcionario
return funcionario?.regimeTrabalho || "clt"; // Default CLT return funcionario?.regimeTrabalho || "clt"; // Default CLT
} }
// Helper: Calcular saldo dinamicamente baseado na tabela ferias
async function calcularSaldo(
ctx: QueryCtx,
funcionarioId: Id<"funcionarios">,
anoReferencia: number,
feriasIdExcluir?: Id<"ferias"> // ID do período a excluir do cálculo (para ajustes)
): Promise<{
diasDireito: number;
diasUsados: number;
diasPendentes: number;
diasDisponiveis: number;
diasAbono: number;
dataInicio: string;
dataFim: string;
status: "ativo" | "vencido" | "concluido";
} | null> {
const funcionario = await ctx.db.get(funcionarioId);
if (!funcionario || !funcionario.admissaoData) return null;
const regime = funcionario.regimeTrabalho || "clt";
const config = REGIMES_CONFIG[regime];
// Calcular anos desde admissão
const dataAdmissao = new Date(funcionario.admissaoData);
const anosDesdeAdmissao = anoReferencia - dataAdmissao.getFullYear();
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
const dataInicio = calcularDataFimPeriodo(
funcionario.admissaoData,
anosDesdeAdmissao - 1
);
const dataFim = calcularDataFimPeriodo(
funcionario.admissaoData,
anosDesdeAdmissao
);
// Buscar todos os registros de férias para este funcionário e ano
const todasFerias = await ctx.db
.query("ferias")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", funcionarioId).eq("anoReferencia", anoReferencia)
)
.collect();
// Filtrar períodos a excluir (para ajustes)
const feriasFiltradas = feriasIdExcluir
? todasFerias.filter((f) => f._id !== feriasIdExcluir)
: todasFerias;
// Calcular dias usados (aprovado, data_ajustada_aprovada, EmFérias)
const diasUsados = feriasFiltradas
.filter(
(f) =>
f.status === "aprovado" ||
f.status === "data_ajustada_aprovada" ||
f.status === "EmFérias"
)
.reduce((acc, f) => acc + f.diasFerias, 0);
// Calcular dias pendentes (aguardando_aprovacao)
const diasPendentes = feriasFiltradas
.filter((f) => f.status === "aguardando_aprovacao")
.reduce((acc, f) => acc + f.diasFerias, 0);
// Calcular dias de abono
const diasAbono = feriasFiltradas.reduce((acc, f) => acc + f.diasAbono, 0);
// Calcular dias disponíveis
const diasDireito = 30;
const diasDisponiveis = diasDireito - diasUsados - diasPendentes - diasAbono;
// Determinar status do período
const hoje = new Date();
const dataFimPeriodo = new Date(dataFim);
let status: "ativo" | "vencido" | "concluido";
if (diasDireito - diasUsados - diasAbono <= 0) {
status = "concluido";
} else if (hoje > dataFimPeriodo) {
status = "vencido";
} else {
status = "ativo";
}
return {
diasDireito,
diasUsados,
diasPendentes,
diasDisponiveis,
diasAbono,
dataInicio,
dataFim,
status,
};
}
/** /**
* Query: Obter saldo de férias de um funcionário para um ano específico * Query: Obter saldo de férias de um funcionário para um ano específico
*/ */
@@ -123,67 +221,17 @@ export const obterSaldo = query({
v.null() v.null()
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Buscar período aquisitivo const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia);
const periodo = await ctx.db if (!saldo) return null;
.query("periodosAquisitivos")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
)
.first();
if (!periodo) {
// Se não existe, calcular e retornar dados previstos sem mutar o banco
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario || !funcionario.admissaoData) return null;
const regime = funcionario.regimeTrabalho || "clt";
const config = REGIMES_CONFIG[regime];
// Calcular anos desde admissão
const dataAdmissao = new Date(funcionario.admissaoData);
const anosDesdeAdmissao = args.anoReferencia - dataAdmissao.getFullYear();
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
const dataInicio = calcularDataFimPeriodo(
funcionario.admissaoData,
anosDesdeAdmissao - 1
);
const dataFim = calcularDataFimPeriodo(
funcionario.admissaoData,
anosDesdeAdmissao
);
return {
anoReferencia: args.anoReferencia,
diasDireito: 30,
diasUsados: 0,
diasPendentes: 0,
diasDisponiveis: 30,
diasAbono: 0,
abonoPermitido: config.abonoPermitido,
status: "ativo" as const,
dataInicio,
dataFim,
regimeTrabalho: config.nome,
};
}
const funcionario = await ctx.db.get(args.funcionarioId); const funcionario = await ctx.db.get(args.funcionarioId);
const regime = funcionario?.regimeTrabalho || "clt"; const regime = funcionario?.regimeTrabalho || "clt";
const config = REGIMES_CONFIG[regime]; const config = REGIMES_CONFIG[regime];
return { return {
anoReferencia: periodo.anoReferencia, anoReferencia: args.anoReferencia,
diasDireito: periodo.diasDireito, ...saldo,
diasUsados: periodo.diasUsados,
diasPendentes: periodo.diasPendentes,
diasDisponiveis: periodo.diasDisponiveis,
diasAbono: periodo.diasAbono,
abonoPermitido: config.abonoPermitido, abonoPermitido: config.abonoPermitido,
status: periodo.status,
dataInicio: periodo.dataInicio,
dataFim: periodo.dataFim,
regimeTrabalho: config.nome, regimeTrabalho: config.nome,
}; };
}, },
@@ -198,7 +246,6 @@ export const listarSaldos = query({
}, },
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("periodosAquisitivos"),
anoReferencia: v.number(), anoReferencia: v.number(),
diasDireito: v.number(), diasDireito: v.number(),
diasUsados: v.number(), diasUsados: v.number(),
@@ -212,24 +259,36 @@ export const listarSaldos = query({
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const periodos = await ctx.db const funcionario = await ctx.db.get(args.funcionarioId);
.query("periodosAquisitivos") if (!funcionario || !funcionario.admissaoData) return [];
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
.collect();
return periodos.map((p) => ({ const regime = funcionario.regimeTrabalho || "clt";
_id: p._id, const config = REGIMES_CONFIG[regime];
anoReferencia: p.anoReferencia,
diasDireito: p.diasDireito, const dataAdmissao = new Date(funcionario.admissaoData);
diasUsados: p.diasUsados, const anoAtual = new Date().getFullYear();
diasPendentes: p.diasPendentes, const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
diasDisponiveis: p.diasDisponiveis,
diasAbono: p.diasAbono, const saldos = [];
abonoPermitido: p.abonoPermitido,
status: p.status, // Calcular saldos para os últimos 3 anos (atual, anterior e anterior ao anterior)
dataInicio: p.dataInicio, for (let i = 0; i < 3; i++) {
dataFim: p.dataFim, const ano = anoAtual - i;
})); const anosPeriodo = ano - dataAdmissao.getFullYear();
if (anosPeriodo < 1) continue;
const saldo = await calcularSaldo(ctx, args.funcionarioId, ano);
if (saldo) {
saldos.push({
anoReferencia: ano,
...saldo,
abonoPermitido: config.abonoPermitido,
});
}
}
return saldos;
}, },
}); });
@@ -246,6 +305,7 @@ export const validarSolicitacao = query({
dataFim: v.string(), dataFim: v.string(),
}) })
), ),
feriasIdExcluir: v.optional(v.id("ferias")), // ID do período a excluir do cálculo de saldo (para ajustes)
}, },
returns: v.object({ returns: v.object({
valido: v.boolean(), valido: v.boolean(),
@@ -287,11 +347,48 @@ export const validarSolicitacao = query({
`Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})` `Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})`
); );
} }
// Validação específica para regime estatutário PE e Municipal
if ((regime === "estatutario_pe" || regime === "estatutario_municipal") && 'periodosPermitidos' in config) {
if (!config.periodosPermitidos.includes(dias)) {
erros.push(
`Para ${config.nome}, os períodos devem ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
);
}
}
}
// Validação específica para regime estatutário PE e Municipal
// Permite períodos fracionados: cada período deve ser 15 ou 30 dias
// Total não pode exceder 30 dias, mas pode ser menos (períodos fracionados)
if ((regime === "estatutario_pe" || regime === "estatutario_municipal")) {
// Verificar se cada período individual é válido (15 ou 30 dias)
for (const dias of diasPorPeriodo) {
if (dias !== 15 && dias !== 30) {
erros.push(
`Para ${config.nome}, cada período deve ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
);
}
}
// Total não pode exceder 30 dias
if (totalDias > 30) {
erros.push(
`Para ${config.nome}, o total de dias não pode exceder 30 dias. Total solicitado: ${totalDias} dias.`
);
}
// Máximo de 2 períodos
if (args.periodos.length > 2) {
erros.push(
`Para ${config.nome}, o máximo de períodos permitidos é 2.`
);
}
} }
// Validação 3: CLT requer um período com 14+ dias se dividir // Validação 3: CLT requer um período com 14+ dias se dividir
if (regime === "clt" && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) { if (regime === "clt" && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) {
const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal); const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal!);
if (!temPeriodo14Dias) { if (!temPeriodo14Dias) {
erros.push( erros.push(
`Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos` `Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos`
@@ -299,43 +396,40 @@ export const validarSolicitacao = query({
} }
} }
// Validação 4: Verificar saldo disponível // Validação 4: Verificar saldo disponível (calculado dinamicamente)
const periodo = await ctx.db // Se for um ajuste (feriasIdExcluir fornecido), excluir esse período do cálculo
.query("periodosAquisitivos") const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia, args.feriasIdExcluir);
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
)
.first();
if (!periodo) { if (!saldo) {
erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`); erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`);
} else { } else {
if (totalDias > periodo.diasDisponiveis) { // Verificar saldo disponível (já excluindo o período original se for ajuste)
if (totalDias > saldo.diasDisponiveis) {
erros.push( erros.push(
`Total solicitado (${totalDias} dias) excede saldo disponível (${periodo.diasDisponiveis} dias)` `Total solicitado (${totalDias} dias) excede saldo disponível (${saldo.diasDisponiveis} dias)`
); );
} }
// Aviso: Saldo baixo // Aviso: Saldo baixo
if (periodo.diasDisponiveis < 15 && periodo.diasDisponiveis > totalDias) { if (saldo.diasDisponiveis < 15 && saldo.diasDisponiveis > totalDias) {
avisos.push( avisos.push(
`Após essa solicitação, restará ${periodo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}` `Após essa solicitação, restará ${saldo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}`
); );
} }
// Aviso: Férias vencendo // Aviso: Férias vencendo
const hoje = new Date(); const hoje = new Date();
const dataFim = new Date(periodo.dataFim); const dataFim = new Date(saldo.dataFim);
const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24)); const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
if (diasAteVencer < 90 && diasAteVencer > 0) { if (diasAteVencer < 90 && diasAteVencer > 0) {
avisos.push( avisos.push(
`⚠️ Atenção: Seu período aquisitivo ${periodo.anoReferencia} vence em ${diasAteVencer} dias!` `⚠️ Atenção: Seu período aquisitivo ${args.anoReferencia} vence em ${diasAteVencer} dias!`
); );
} }
if (diasAteVencer < 0) { if (diasAteVencer < 0) {
avisos.push( avisos.push(
`⚠️ URGENTE: Seu período aquisitivo ${periodo.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!` `⚠️ URGENTE: Seu período aquisitivo ${args.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!`
); );
} }
} }
@@ -388,166 +482,3 @@ export const validarSolicitacao = query({
}; };
}, },
}); });
/**
* Internal Mutation: Atualizar saldo após aprovação de férias
*/
export const atualizarSaldoAposAprovacao = internalMutation({
args: {
solicitacaoId: v.id("solicitacoesFerias"),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) return null;
// Buscar período aquisitivo
const periodo = await ctx.db
.query("periodosAquisitivos")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
)
.first();
if (!periodo) return null;
// Calcular total de dias
let totalDias = 0;
for (const p of solicitacao.periodos) {
totalDias += p.diasCorridos;
}
// Atualizar saldo
await ctx.db.patch(periodo._id, {
diasPendentes: periodo.diasPendentes - totalDias,
diasUsados: periodo.diasUsados + totalDias,
diasDisponiveis: periodo.diasDireito - (periodo.diasUsados + totalDias) - periodo.diasAbono,
status: periodo.diasDireito - (periodo.diasUsados + totalDias) <= 0 ? "concluido" : periodo.status,
});
return null;
},
});
/**
* Internal Mutation: Reservar dias (ao criar solicitação)
*/
export const reservarDias = internalMutation({
args: {
funcionarioId: v.id("funcionarios"),
anoReferencia: v.number(),
totalDias: v.number(),
},
returns: v.null(),
handler: async (ctx, args) => {
const periodo = await ctx.db
.query("periodosAquisitivos")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", args.funcionarioId).eq("anoReferencia", args.anoReferencia)
)
.first();
if (!periodo) return null;
await ctx.db.patch(periodo._id, {
diasPendentes: periodo.diasPendentes + args.totalDias,
diasDisponiveis: periodo.diasDisponiveis - args.totalDias,
});
return null;
},
});
/**
* Internal Mutation: Liberar dias (ao reprovar solicitação)
*/
export const liberarDias = internalMutation({
args: {
solicitacaoId: v.id("solicitacoesFerias"),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) return null;
const periodo = await ctx.db
.query("periodosAquisitivos")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", solicitacao.funcionarioId).eq("anoReferencia", solicitacao.anoReferencia)
)
.first();
if (!periodo) return null;
let totalDias = 0;
for (const p of solicitacao.periodos) {
totalDias += p.diasCorridos;
}
await ctx.db.patch(periodo._id, {
diasPendentes: periodo.diasPendentes - totalDias,
diasDisponiveis: periodo.diasDisponiveis + totalDias,
});
return null;
},
});
/**
* Internal Mutation: Criar períodos aquisitivos para todos os funcionários
*/
export const criarPeriodosAquisitivos = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const funcionarios = await ctx.db.query("funcionarios").collect();
const anoAtual = new Date().getFullYear();
for (const func of funcionarios) {
if (!func.admissaoData) continue;
const regime = func.regimeTrabalho || "clt";
const config = REGIMES_CONFIG[regime];
const dataAdmissao = new Date(func.admissaoData);
const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
// Criar períodos para os últimos 2 anos (atual e anterior)
for (let i = 0; i < 2; i++) {
const ano = anoAtual - i;
const anosPeriodo = ano - dataAdmissao.getFullYear();
if (anosPeriodo < 1) continue;
// Verificar se já existe
const periodoExistente = await ctx.db
.query("periodosAquisitivos")
.withIndex("by_funcionario_and_ano", (q) =>
q.eq("funcionarioId", func._id).eq("anoReferencia", ano)
)
.first();
if (periodoExistente) continue;
const dataInicio = calcularDataFimPeriodo(func.admissaoData, anosPeriodo - 1);
const dataFim = calcularDataFimPeriodo(func.admissaoData, anosPeriodo);
await ctx.db.insert("periodosAquisitivos", {
funcionarioId: func._id,
anoReferencia: ano,
dataInicio,
dataFim,
diasDireito: 30,
diasUsados: 0,
diasPendentes: 0,
diasDisponiveis: 30,
abonoPermitido: config.abonoPermitido,
diasAbono: 0,
status: "ativo",
});
}
}
return null;
},
});

View File

@@ -208,54 +208,44 @@ export default defineSchema({
.index("by_licenca_original", ["licencaOriginalId"]) .index("by_licenca_original", ["licencaOriginalId"])
.index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]), .index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]),
solicitacoesFerias: defineTable({ ferias: defineTable({
funcionarioId: v.id("funcionarios"), funcionarioId: v.id("funcionarios"),
anoReferencia: v.number(), anoReferencia: v.number(),
dataInicio: v.string(),
dataFim: v.string(),
diasFerias: v.number(),
status: v.union( status: v.union(
v.literal("aguardando_aprovacao"), v.literal("aguardando_aprovacao"),
v.literal("aprovado"), v.literal("aprovado"),
v.literal("reprovado"), v.literal("reprovado"),
v.literal("data_ajustada_aprovada") v.literal("data_ajustada_aprovada"),
), v.literal("EmFérias")
periodos: v.array(
v.object({
dataInicio: v.string(),
dataFim: v.string(),
diasCorridos: v.number(),
})
), ),
gestorId: v.optional(v.id("usuarios")),
observacao: v.optional(v.string()), observacao: v.optional(v.string()),
motivoReprovacao: v.optional(v.string()), motivoReprovacao: v.optional(v.string()),
gestorId: v.optional(v.id("usuarios")),
dataAprovacao: v.optional(v.number()), dataAprovacao: v.optional(v.number()),
dataReprovacao: v.optional(v.number()), dataReprovacao: v.optional(v.number()),
diasAbono: v.number(),
historicoAlteracoes: v.optional( historicoAlteracoes: v.optional(
v.array( v.array(
v.object({ v.object({
data: v.number(), data: v.number(),
usuarioId: v.id("usuarios"), usuarioId: v.id("usuarios"),
acao: v.string(), acao: v.string(),
periodosAnteriores: v.optional(
v.array(
v.object({
dataInicio: v.string(),
dataFim: v.string(),
diasCorridos: v.number(),
})
)
),
}) })
) )
), ),
}) })
.index("by_funcionario", ["funcionarioId"]) .index("by_funcionario", ["funcionarioId"])
.index("by_status", ["status"]) .index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"])
.index("by_funcionario_and_status", ["funcionarioId", "status"]) .index("by_funcionario_and_status", ["funcionarioId", "status"])
.index("by_status", ["status"])
.index("by_ano", ["anoReferencia"]), .index("by_ano", ["anoReferencia"]),
notificacoesFerias: defineTable({ notificacoesFerias: defineTable({
destinatarioId: v.id("usuarios"), destinatarioId: v.id("usuarios"),
solicitacaoFeriasId: v.id("solicitacoesFerias"), feriasId: v.id("ferias"),
tipo: v.union( tipo: v.union(
v.literal("nova_solicitacao"), v.literal("nova_solicitacao"),
v.literal("aprovado"), v.literal("aprovado"),
@@ -304,27 +294,6 @@ export default defineSchema({
.index("by_destinatario", ["destinatarioId"]) .index("by_destinatario", ["destinatarioId"])
.index("by_destinatario_and_lida", ["destinatarioId", "lida"]), .index("by_destinatario_and_lida", ["destinatarioId", "lida"]),
// Períodos aquisitivos e saldos de férias
periodosAquisitivos: defineTable({
funcionarioId: v.id("funcionarios"),
anoReferencia: v.number(), // Ano do período aquisitivo (ex: 2024)
dataInicio: v.string(), // Data de início do período aquisitivo
dataFim: v.string(), // Data de fim do período aquisitivo
diasDireito: v.number(), // Dias de férias que tem direito (30 ou proporcional)
diasUsados: v.number(), // Dias já usados
diasPendentes: v.number(), // Dias em solicitações aguardando aprovação
diasDisponiveis: v.number(), // Dias disponíveis = direito - usados - pendentes
abonoPermitido: v.boolean(), // Se pode vender 1/3 das férias
diasAbono: v.number(), // Dias vendidos como abono pecuniário
status: v.union(
v.literal("ativo"), // Período vigente
v.literal("vencido"), // Período vencido (não tirou férias)
v.literal("concluido") // Período totalmente utilizado
),
})
.index("by_funcionario", ["funcionarioId"])
.index("by_funcionario_and_ano", ["funcionarioId", "anoReferencia"])
.index("by_funcionario_and_status", ["funcionarioId", "status"]),
times: defineTable({ times: defineTable({
nome: v.string(), nome: v.string(),

View File

@@ -579,25 +579,15 @@ export const clearDatabase = internalMutation({
`${notificacoesFerias.length} notificações de férias removidas` `${notificacoesFerias.length} notificações de férias removidas`
); );
// 4. Férias e períodos aquisitivos // 4. Férias
const solicitacoesFerias = await ctx.db const ferias = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.collect(); .collect();
for (const solicitacao of solicitacoesFerias) { for (const feriasRegistro of ferias) {
await ctx.db.delete(solicitacao._id); await ctx.db.delete(feriasRegistro._id);
} }
console.log( console.log(
`${solicitacoesFerias.length} solicitações de férias removidas` `${ferias.length} registros de férias removidos`
);
const periodosAquisitivos = await ctx.db
.query("periodosAquisitivos")
.collect();
for (const periodo of periodosAquisitivos) {
await ctx.db.delete(periodo._id);
}
console.log(
`${periodosAquisitivos.length} períodos aquisitivos removidos`
); );
// 5. Atestados // 5. Atestados
@@ -849,25 +839,15 @@ export const limparBanco = mutation({
`${notificacoesFerias.length} notificações de férias removidas` `${notificacoesFerias.length} notificações de férias removidas`
); );
// 4. Férias e períodos aquisitivos // 4. Férias
const solicitacoesFerias = await ctx.db const ferias = await ctx.db
.query("solicitacoesFerias") .query("ferias")
.collect(); .collect();
for (const solicitacao of solicitacoesFerias) { for (const feriasRegistro of ferias) {
await ctx.db.delete(solicitacao._id); await ctx.db.delete(feriasRegistro._id);
} }
console.log( console.log(
`${solicitacoesFerias.length} solicitações de férias removidas` `${ferias.length} registros de férias removidos`
);
const periodosAquisitivos = await ctx.db
.query("periodosAquisitivos")
.collect();
for (const periodo of periodosAquisitivos) {
await ctx.db.delete(periodo._id);
}
console.log(
`${periodosAquisitivos.length} períodos aquisitivos removidos`
); );
// 5. Atestados // 5. Atestados