Correcao ferias #23
@@ -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,34 +85,29 @@
|
|||||||
</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 class="badge badge-primary">{index + 1}</div>
|
|
||||||
<div class="grid flex-1 grid-cols-3 gap-2 text-sm">
|
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Início:</span>
|
<span class="text-base-content/70">Início:</span>
|
||||||
<span class="ml-1 font-semibold"
|
<span class="ml-1 font-semibold"
|
||||||
>{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')}</span
|
>{new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Fim:</span>
|
<span class="text-base-content/70">Fim:</span>
|
||||||
<span class="ml-1 font-semibold"
|
<span class="ml-1 font-semibold"
|
||||||
>{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}</span
|
>{new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Dias:</span>
|
<span class="text-base-content/70">Dias:</span>
|
||||||
<span class="text-primary ml-1 font-bold">{periodo.diasCorridos}</span>
|
<span class="text-primary ml-1 font-bold">{solicitacao.diasFerias}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Observações -->
|
<!-- Observações -->
|
||||||
|
|||||||
@@ -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 class="badge badge-primary">{index + 1}</div>
|
|
||||||
<div class="grid flex-1 grid-cols-3 gap-2 text-sm">
|
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Início:</span>
|
<span class="text-base-content/70">Início:</span>
|
||||||
<span class="ml-1 font-semibold"
|
<span class="ml-1 font-semibold"
|
||||||
>{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')}</span
|
>{formatarDataString(periodo.dataInicio)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Fim:</span>
|
<span class="text-base-content/70">Fim:</span>
|
||||||
<span class="ml-1 font-semibold"
|
<span class="ml-1 font-semibold"
|
||||||
>{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}</span
|
>{formatarDataString(periodo.dataFim)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-base-content/70">Dias:</span>
|
<span class="text-base-content/70">Dias:</span>
|
||||||
<span class="text-primary ml-1 font-bold">{periodo.diasCorridos}</span>
|
<span class="text-primary ml-1 font-bold">{periodo.diasFerias}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
</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">
|
||||||
<h5 class="mb-2 font-medium">Período {index + 1}</h5>
|
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for={`ajuste-inicio-${index}`}>
|
<label class="label" for="ajuste-inicio">
|
||||||
<span class="label-text text-xs">Início</span>
|
<span class="label-text text-xs">Início</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={`ajuste-inicio-${index}`}
|
id="ajuste-inicio"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
bind:value={periodo.dataInicio}
|
bind:value={novaDataInicio}
|
||||||
onchange={() => calcularDias(periodo)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for={`ajuste-fim-${index}`}>
|
<label class="label" for="ajuste-fim">
|
||||||
<span class="label-text text-xs">Fim</span>
|
<span class="label-text text-xs">Fim</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={`ajuste-fim-${index}`}
|
id="ajuste-fim"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
bind:value={periodo.dataFim}
|
bind:value={novaDataFim}
|
||||||
onchange={() => calcularDias(periodo)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for={`ajuste-dias-${index}`}>
|
<label class="label" for="ajuste-dias">
|
||||||
<span class="label-text text-xs">Dias</span>
|
<span class="label-text text-xs">Dias</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
id={`ajuste-dias-${index}`}
|
id="ajuste-dias"
|
||||||
class="bg-base-300 flex h-9 items-center rounded-lg px-3"
|
class="bg-base-300 flex h-9 items-center rounded-lg px-3"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
>
|
>
|
||||||
<span class="font-bold">{periodo.diasCorridos}</span>
|
<span class="font-bold">{diasAjustados}</span>
|
||||||
|
<span class="ml-2 text-xs opacity-70">dias</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
|
|
||||||
<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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,27 +2005,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de Aprovação de Férias -->
|
<!-- Modal de Aprovação de Férias -->
|
||||||
{#if solicitacaoSelecionada}
|
{#if periodoSelecionado && currentUser.data}
|
||||||
|
{#await client.query(api.ferias.obterDetalhes, { feriasId: periodoSelecionado }) then detalhes}
|
||||||
|
{#if detalhes}
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box max-w-4xl">
|
<div class="modal-box max-w-4xl">
|
||||||
{#if currentUser.data}
|
|
||||||
<AprovarFerias
|
<AprovarFerias
|
||||||
solicitacao={solicitacaoSelecionada}
|
periodo={detalhes}
|
||||||
gestorId={currentUser.data._id}
|
gestorId={currentUser.data._id}
|
||||||
onSucesso={recarregar}
|
onSucesso={recarregar}
|
||||||
onCancelar={() => (solicitacaoSelecionada = null)}
|
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}
|
{/if}
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Modal de Upload de Foto / Escolher Avatar -->
|
<!-- Modal de Upload de Foto / Escolher Avatar -->
|
||||||
{#if mostrarModalFoto}
|
{#if mostrarModalFoto}
|
||||||
|
|||||||
@@ -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,7 +165,6 @@
|
|||||||
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) {
|
||||||
@@ -185,7 +184,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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.diasCorridos
|
diasCorridos: periodo.diasFerias
|
||||||
}))
|
}))
|
||||||
)
|
|
||||||
.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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -229,24 +229,21 @@ 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 }) => {
|
|
||||||
const dias = calcularDias(p.dataInicio, p.dataFim);
|
|
||||||
totalDiasPorTipo.ferias += dias;
|
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) {
|
|
||||||
if (!periodo.dataInicio || !periodo.dataFim) continue;
|
|
||||||
|
|
||||||
eventos.push({
|
eventos.push({
|
||||||
id: `ferias-${solicitacao._id}-${periodo.dataInicio}`,
|
id: `ferias-${feriasRegistro._id}`,
|
||||||
title: `${funcionario.nome} - Férias`,
|
title: `${funcionario.nome} - Férias`,
|
||||||
start: periodo.dataInicio,
|
start: feriasRegistro.dataInicio,
|
||||||
end: periodo.dataFim,
|
end: feriasRegistro.dataFim,
|
||||||
color: '#10b981', // verde
|
color: '#10b981', // verde
|
||||||
tipo: 'ferias',
|
tipo: 'ferias',
|
||||||
funcionarioNome: funcionario.nome,
|
funcionarioNome: funcionario.nome,
|
||||||
funcionarioId: funcionario._id
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,42 +95,35 @@ 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();
|
||||||
@@ -81,20 +133,16 @@ export const listarMinhasSolicitacoes = query({
|
|||||||
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) => ({
|
||||||
|
...ferias,
|
||||||
funcionario,
|
funcionario,
|
||||||
time,
|
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);
|
||||||
|
|
||||||
// Query: Obter detalhes completos de uma solicitação
|
// Buscar time do funcionário
|
||||||
// Retorna tipo inferido automaticamente pelo Convex
|
const membroTime = await ctx.db
|
||||||
export const obterDetalhes = query({
|
.query("timesMembros")
|
||||||
args: { solicitacaoId: v.id("solicitacoesFerias") },
|
.withIndex("by_funcionario", (q) =>
|
||||||
handler: async (ctx, args) => {
|
q.eq("funcionarioId", ferias.funcionarioId)
|
||||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
)
|
||||||
if (!solicitacao) return null;
|
.filter((q) => q.eq(q.field("ativo"), true))
|
||||||
|
.first();
|
||||||
|
|
||||||
const funcionario = await ctx.db.get(solicitacao.funcionarioId);
|
let time = null;
|
||||||
let gestor = null;
|
if (membroTime) {
|
||||||
if (solicitacao.gestorId) {
|
time = await ctx.db.get(membroTime.timeId);
|
||||||
gestor = await ctx.db.get(solicitacao.gestorId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...solicitacao,
|
...ferias,
|
||||||
|
funcionario,
|
||||||
|
time,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return periodosComDetalhes.sort((a, b) => b._creationTime - a._creationTime);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query: Obter detalhes de um período individual
|
||||||
|
export const obterDetalhes = query({
|
||||||
|
args: {
|
||||||
|
feriasId: v.id("ferias")
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ferias = await ctx.db.get(args.feriasId);
|
||||||
|
|
||||||
|
if (!ferias) return null;
|
||||||
|
|
||||||
|
const funcionario = await ctx.db.get(ferias.funcionarioId);
|
||||||
|
let gestor = null;
|
||||||
|
if (ferias.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 {
|
||||||
|
...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,
|
|
||||||
status: "aguardando_aprovacao",
|
|
||||||
periodos: args.periodos,
|
|
||||||
observacao: args.observacao,
|
|
||||||
historicoAlteracoes: [
|
|
||||||
{
|
{
|
||||||
data: Date.now(),
|
data: Date.now(),
|
||||||
usuarioId: usuario?._id || funcionario.gestorId!,
|
usuarioId: usuario?._id || funcionario.gestorId!,
|
||||||
acao: "Solicitação criada",
|
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,15 +529,22 @@ 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);
|
||||||
|
|
||||||
@@ -480,7 +552,6 @@ export const verificarStatusFerias = query({
|
|||||||
return "em_ferias";
|
return "em_ferias";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return "ativo";
|
return "ativo";
|
||||||
},
|
},
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user