442 lines
12 KiB
Svelte
442 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import UserAvatar from './chat/UserAvatar.svelte';
|
|
import { Clock, Check, Edit, X, XCircle } from 'lucide-svelte';
|
|
|
|
type PeriodoFerias = Doc<'ferias'> & {
|
|
funcionario?: Doc<'funcionarios'> | null;
|
|
gestor?: Doc<'usuarios'> | null;
|
|
time?: Doc<'times'> | null;
|
|
};
|
|
|
|
interface Props {
|
|
periodo: PeriodoFerias;
|
|
gestorId: Id<'usuarios'>;
|
|
onSucesso?: () => void;
|
|
onCancelar?: () => void;
|
|
}
|
|
|
|
let { periodo, gestorId, onSucesso, onCancelar }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
|
|
let modoAjuste = $state(false);
|
|
let novaDataInicio = $state(periodo.dataInicio);
|
|
let novaDataFim = $state(periodo.dataFim);
|
|
let motivoReprovacao = $state('');
|
|
let processando = $state(false);
|
|
let erro = $state('');
|
|
|
|
// Calcular dias do período ajustado
|
|
const diasAjustados = $derived.by(() => {
|
|
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(dataInicio: string, dataFim: string): number {
|
|
if (!dataInicio || !dataFim) return 0;
|
|
|
|
const inicio = new Date(dataInicio);
|
|
const fim = new Date(dataFim);
|
|
|
|
if (fim < inicio) {
|
|
erro = 'Data final não pode ser anterior à data inicial';
|
|
return 0;
|
|
}
|
|
|
|
const diff = fim.getTime() - inicio.getTime();
|
|
const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
|
|
erro = '';
|
|
return dias;
|
|
}
|
|
|
|
async function aprovar() {
|
|
try {
|
|
processando = true;
|
|
erro = '';
|
|
|
|
// Validar se as datas e condições estão dentro do regime do funcionário
|
|
if (!periodo.funcionario?._id) {
|
|
erro = 'Funcionário não encontrado';
|
|
processando = false;
|
|
return;
|
|
}
|
|
|
|
const validacao = await client.query(api.saldoFerias.validarSolicitacao, {
|
|
funcionarioId: periodo.funcionario._id,
|
|
anoReferencia: periodo.anoReferencia,
|
|
periodos: [
|
|
{
|
|
dataInicio: periodo.dataInicio,
|
|
dataFim: periodo.dataFim
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!validacao.valido) {
|
|
erro = `Não é possível aprovar: ${validacao.erros.join('; ')}`;
|
|
processando = false;
|
|
return;
|
|
}
|
|
|
|
await client.mutation(api.ferias.aprovar, {
|
|
feriasId: periodo._id,
|
|
gestorId: gestorId
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e) {
|
|
erro = e instanceof Error ? e.message : String(e);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function reprovar() {
|
|
if (!motivoReprovacao.trim()) {
|
|
erro = 'Informe o motivo da reprovação';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
processando = true;
|
|
erro = '';
|
|
|
|
await client.mutation(api.ferias.reprovar, {
|
|
feriasId: periodo._id,
|
|
gestorId: gestorId,
|
|
motivoReprovacao
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e) {
|
|
erro = e instanceof Error ? e.message : String(e);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function ajustarEAprovar() {
|
|
try {
|
|
processando = true;
|
|
erro = '';
|
|
|
|
// Validar se as datas ajustadas e condições estão dentro do regime do funcionário
|
|
if (!periodo.funcionario?._id) {
|
|
erro = 'Funcionário não encontrado';
|
|
processando = false;
|
|
return;
|
|
}
|
|
|
|
// Validar datas ajustadas
|
|
if (!novaDataInicio || !novaDataFim) {
|
|
erro = 'Informe as novas datas de início e fim';
|
|
processando = false;
|
|
return;
|
|
}
|
|
|
|
const validacao = await client.query(api.saldoFerias.validarSolicitacao, {
|
|
funcionarioId: periodo.funcionario._id,
|
|
anoReferencia: periodo.anoReferencia,
|
|
periodos: [
|
|
{
|
|
dataInicio: novaDataInicio,
|
|
dataFim: novaDataFim
|
|
}
|
|
],
|
|
feriasIdExcluir: periodo._id // Excluir o período original do cálculo de saldo
|
|
});
|
|
|
|
if (!validacao.valido) {
|
|
erro = `Não é possível aprovar com ajuste: ${validacao.erros.join('; ')}`;
|
|
processando = false;
|
|
return;
|
|
}
|
|
|
|
await client.mutation(api.ferias.ajustarEAprovar, {
|
|
feriasId: periodo._id,
|
|
gestorId: gestorId,
|
|
novaDataInicio,
|
|
novaDataFim
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e) {
|
|
erro = e instanceof Error ? e.message : String(e);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function getStatusBadge(status: string) {
|
|
const badges: Record<string, string> = {
|
|
aguardando_aprovacao: 'badge-warning',
|
|
aprovado: 'badge-success',
|
|
reprovado: 'badge-error',
|
|
data_ajustada_aprovada: 'badge-info',
|
|
EmFérias: 'badge-info'
|
|
};
|
|
return badges[status] || 'badge-neutral';
|
|
}
|
|
|
|
function getStatusTexto(status: string) {
|
|
const textos: Record<string, string> = {
|
|
aguardando_aprovacao: 'Aguardando Aprovação',
|
|
aprovado: 'Aprovado',
|
|
reprovado: 'Reprovado',
|
|
data_ajustada_aprovada: 'Data Ajustada e Aprovada',
|
|
EmFérias: 'Em Férias'
|
|
};
|
|
return textos[status] || status;
|
|
}
|
|
|
|
function formatarData(data: number) {
|
|
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>
|
|
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="mb-4 flex items-start justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<UserAvatar
|
|
fotoPerfilUrl={periodo.funcionario?.fotoPerfilUrl}
|
|
nome={periodo.funcionario?.nome || 'Funcionário'}
|
|
size="md"
|
|
/>
|
|
<div>
|
|
<h2 class="card-title text-2xl">
|
|
{periodo.funcionario?.nome || 'Funcionário'}
|
|
</h2>
|
|
<p class="text-base-content/70 mt-1 text-sm">
|
|
Ano de Referência: {periodo.anoReferencia}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class={`badge ${getStatusBadge(periodo.status)} badge-lg`}>
|
|
{getStatusTexto(periodo.status)}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Período Solicitado -->
|
|
<div class="mt-4">
|
|
<h3 class="mb-3 text-lg font-semibold">Período Solicitado</h3>
|
|
<div class="bg-base-200 rounded-lg p-4">
|
|
<div class="grid grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<span class="text-base-content/70">Início:</span>
|
|
<span class="ml-1 font-semibold">{formatarDataString(periodo.dataInicio)}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-base-content/70">Fim:</span>
|
|
<span class="ml-1 font-semibold">{formatarDataString(periodo.dataFim)}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-base-content/70">Dias:</span>
|
|
<span class="text-primary ml-1 font-bold">{periodo.diasFerias}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Observações -->
|
|
{#if periodo.observacao}
|
|
<div class="mt-4">
|
|
<h3 class="mb-2 font-semibold">Observações</h3>
|
|
<div class="bg-base-200 rounded-lg p-3 text-sm">
|
|
{periodo.observacao}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Histórico -->
|
|
{#if periodo.historicoAlteracoes && periodo.historicoAlteracoes.length > 0}
|
|
<div class="mt-4">
|
|
<h3 class="mb-2 font-semibold">Histórico</h3>
|
|
<div class="space-y-1">
|
|
{#each periodo.historicoAlteracoes as hist}
|
|
<div class="text-base-content/70 flex items-center gap-2 text-xs">
|
|
<Clock class="h-3 w-3" strokeWidth={2} />
|
|
<span>{formatarData(hist.data)}</span>
|
|
<span>-</span>
|
|
<span>{hist.acao}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Ações (apenas para status aguardando_aprovacao) -->
|
|
{#if periodo.status === 'aguardando_aprovacao'}
|
|
<div class="divider mt-6"></div>
|
|
|
|
{#if !modoAjuste}
|
|
<!-- Modo Normal -->
|
|
<div class="space-y-4">
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-success gap-2"
|
|
onclick={aprovar}
|
|
disabled={processando}
|
|
>
|
|
<Check class="h-5 w-5" strokeWidth={2} />
|
|
Aprovar
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-info gap-2"
|
|
onclick={() => (modoAjuste = true)}
|
|
disabled={processando}
|
|
>
|
|
<Edit class="h-5 w-5" strokeWidth={2} />
|
|
Ajustar Datas e Aprovar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Reprovar -->
|
|
<div class="card bg-base-200">
|
|
<div class="card-body p-4">
|
|
<h4 class="mb-2 text-sm font-semibold">Reprovar Período</h4>
|
|
<textarea
|
|
class="textarea textarea-bordered textarea-sm mb-2"
|
|
placeholder="Motivo da reprovação..."
|
|
bind:value={motivoReprovacao}
|
|
rows="2"
|
|
></textarea>
|
|
<button
|
|
type="button"
|
|
class="btn btn-error btn-sm gap-2"
|
|
onclick={reprovar}
|
|
disabled={processando || !motivoReprovacao.trim()}
|
|
>
|
|
<X class="h-4 w-4" strokeWidth={2} />
|
|
Reprovar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- Modo Ajuste -->
|
|
<div class="space-y-4">
|
|
<h4 class="font-semibold">Ajustar Período</h4>
|
|
<div class="card bg-base-200">
|
|
<div class="card-body p-4">
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div class="form-control">
|
|
<label class="label" for="ajuste-inicio">
|
|
<span class="label-text text-xs">Início</span>
|
|
</label>
|
|
<input
|
|
id="ajuste-inicio"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={novaDataInicio}
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for="ajuste-fim">
|
|
<span class="label-text text-xs">Fim</span>
|
|
</label>
|
|
<input
|
|
id="ajuste-fim"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={novaDataFim}
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for="ajuste-dias">
|
|
<span class="label-text text-xs">Dias</span>
|
|
</label>
|
|
<div
|
|
id="ajuste-dias"
|
|
class="bg-base-300 flex h-9 items-center rounded-lg px-3"
|
|
role="textbox"
|
|
aria-readonly="true"
|
|
>
|
|
<span class="font-bold">{diasAjustados}</span>
|
|
<span class="ml-2 text-xs opacity-70">dias</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
onclick={() => (modoAjuste = false)}
|
|
disabled={processando}
|
|
>
|
|
Cancelar Ajuste
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm gap-2"
|
|
onclick={ajustarEAprovar}
|
|
disabled={processando || !novaDataInicio || !novaDataFim || diasAjustados <= 0}
|
|
>
|
|
<Check class="h-4 w-4" strokeWidth={2} />
|
|
Confirmar e Aprovar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- Motivo Reprovação (se reprovado) -->
|
|
{#if periodo.status === 'reprovado' && periodo.motivoReprovacao}
|
|
<div class="alert alert-error mt-4">
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div>
|
|
<div class="font-bold">Motivo da Reprovação:</div>
|
|
<div class="text-sm">{periodo.motivoReprovacao}</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Erro -->
|
|
{#if erro}
|
|
<div class="alert alert-error mt-4">
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" />
|
|
<span>{erro}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Botão Fechar -->
|
|
{#if onCancelar}
|
|
<div class="card-actions mt-4 justify-end">
|
|
<button type="button" class="btn" onclick={onCancelar} disabled={processando}>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|