386 lines
11 KiB
Svelte
386 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
|
import { SvelteDate } from 'svelte/reactivity';
|
|
import { Check, ChevronLeft, ChevronRight, Calendar, AlertTriangle, CheckCircle } from 'lucide-svelte';
|
|
import { parseLocalDate } from '$lib/utils/datas';
|
|
import type { toast } from 'svelte-sonner';
|
|
import ErrorModal from '../ErrorModal.svelte';
|
|
import CalendarioAusencias from './CalendarioAusencias.svelte';
|
|
|
|
interface Props {
|
|
funcionarioId: Id<'funcionarios'>;
|
|
onSucesso?: () => void;
|
|
onCancelar?: () => void;
|
|
}
|
|
|
|
const { funcionarioId, onSucesso, onCancelar }: Props = $props();
|
|
|
|
// Cliente Convex
|
|
const client = useConvexClient();
|
|
|
|
// Estado do wizard
|
|
let passoAtual = $state(1);
|
|
const totalPassos = 2;
|
|
|
|
// Dados da solicitação
|
|
let dataInicio = $state<string>('');
|
|
let dataFim = $state<string>('');
|
|
let motivo = $state('');
|
|
let processando = $state(false);
|
|
|
|
// Estados para modal de erro
|
|
let mostrarModalErro = $state(false);
|
|
let mensagemErroModal = $state('');
|
|
let detalhesErroModal = $state('');
|
|
|
|
// Buscar ausências existentes para exibir no calendário
|
|
const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, {
|
|
funcionarioId
|
|
});
|
|
|
|
// Filtrar apenas ausências aprovadas ou aguardando aprovação (que bloqueiam novas solicitações)
|
|
let ausenciasExistentes = $derived(
|
|
(ausenciasExistentesQuery?.data || [])
|
|
.filter((a) => a.status === 'aprovado' || a.status === 'aguardando_aprovacao')
|
|
.map((a) => ({
|
|
dataInicio: a.dataInicio,
|
|
dataFim: a.dataFim,
|
|
status: a.status as 'aguardando_aprovacao' | 'aprovado'
|
|
}))
|
|
);
|
|
|
|
// Calcular dias selecionados
|
|
function calcularDias(inicio: string, fim: string): number {
|
|
if (!inicio || !fim) return 0;
|
|
const dInicio = new Date(inicio);
|
|
const dFim = new Date(fim);
|
|
const diffTime = Math.abs(dFim.getTime() - dInicio.getTime());
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
}
|
|
|
|
let totalDias = $derived(calcularDias(dataInicio, dataFim));
|
|
|
|
// Funções de navegação
|
|
function proximoPasso() {
|
|
if (passoAtual === 1) {
|
|
if (!dataInicio || !dataFim) {
|
|
toast.error('Selecione o período de ausência no calendário');
|
|
return;
|
|
}
|
|
|
|
const hoje = new SvelteDate();
|
|
hoje.setHours(0, 0, 0, 0);
|
|
const inicio = parseLocalDate(dataInicio);
|
|
|
|
if (inicio < hoje) {
|
|
toast.error('A data de início não pode ser no passado');
|
|
return;
|
|
}
|
|
|
|
if (parseLocalDate(dataFim) < parseLocalDate(dataInicio)) {
|
|
toast.error('A data de fim deve ser maior ou igual à data de início');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (passoAtual < totalPassos) {
|
|
passoAtual++;
|
|
}
|
|
}
|
|
|
|
function passoAnterior() {
|
|
if (passoAtual > 1) {
|
|
passoAtual--;
|
|
}
|
|
}
|
|
|
|
async function enviarSolicitacao() {
|
|
if (!dataInicio || !dataFim) {
|
|
toast.error('Selecione o período de ausência');
|
|
return;
|
|
}
|
|
|
|
if (!motivo.trim() || motivo.trim().length < 10) {
|
|
toast.error('O motivo deve ter no mínimo 10 caracteres');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
processando = true;
|
|
mostrarModalErro = false;
|
|
mensagemErroModal = '';
|
|
|
|
await client.mutation(api.ausencias.criarSolicitacao, {
|
|
funcionarioId,
|
|
dataInicio,
|
|
dataFim,
|
|
motivo: motivo.trim()
|
|
});
|
|
|
|
toast.success('Solicitação de ausência criada com sucesso!');
|
|
|
|
if (onSucesso) {
|
|
onSucesso();
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao criar solicitação:', error);
|
|
const mensagemErro = error instanceof Error ? error.message : String(error);
|
|
|
|
// Verificar se é erro de sobreposição de período
|
|
if (
|
|
mensagemErro.includes('Já existe uma solicitação') ||
|
|
mensagemErro.includes('já existe') ||
|
|
mensagemErro.includes('solicitação aprovada ou pendente')
|
|
) {
|
|
mensagemErroModal = 'Não é possível criar esta solicitação.';
|
|
detalhesErroModal = `Já existe uma solicitação aprovada ou pendente para o período selecionado:\n\nPeríodo selecionado: ${parseLocalDate(dataInicio).toLocaleDateString('pt-BR')} até ${parseLocalDate(dataFim).toLocaleDateString('pt-BR')}\n\nPor favor, escolha um período diferente ou aguarde a resposta da solicitação existente.`;
|
|
mostrarModalErro = true;
|
|
} else {
|
|
// Outros erros continuam usando toast
|
|
toast.error(mensagemErro);
|
|
}
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function fecharModalErro() {
|
|
mostrarModalErro = false;
|
|
mensagemErroModal = '';
|
|
detalhesErroModal = '';
|
|
}
|
|
|
|
function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) {
|
|
dataInicio = periodo.dataInicio;
|
|
dataFim = periodo.dataFim;
|
|
}
|
|
</script>
|
|
|
|
<div class="wizard-ausencia">
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<p class="text-base-content/70">Solicite uma ausência para assuntos particulares</p>
|
|
</div>
|
|
|
|
<!-- Indicador de progresso -->
|
|
<div class="steps mb-8">
|
|
<div class="step {passoAtual >= 1 ? 'step-primary' : ''}">
|
|
<div class="step-item">
|
|
<div class="step-marker">
|
|
{#if passoAtual > 1}
|
|
<Check class="h-6 w-6" strokeWidth={2} />
|
|
{:else}
|
|
{passoAtual}
|
|
{/if}
|
|
</div>
|
|
<div class="step-content">
|
|
<div class="step-title">Selecionar Período</div>
|
|
<div class="step-description">Escolha as datas no calendário</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="step {passoAtual >= 2 ? 'step-primary' : ''}">
|
|
<div class="step-item">
|
|
<div class="step-marker">
|
|
{#if passoAtual > 2}
|
|
<Check class="h-6 w-6" strokeWidth={2} />
|
|
{:else}
|
|
2
|
|
{/if}
|
|
</div>
|
|
<div class="step-content">
|
|
<div class="step-title">Informar Motivo</div>
|
|
<div class="step-description">Descreva o motivo da ausência</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conteúdo dos passos -->
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
{#if passoAtual === 1}
|
|
<!-- Passo 1: Selecionar Período -->
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="mb-2 text-2xl font-bold">Selecione o Período</h3>
|
|
<p class="text-base-content/70">
|
|
Clique e arraste no calendário para selecionar o período de ausência
|
|
</p>
|
|
</div>
|
|
|
|
{#if ausenciasExistentesQuery === undefined}
|
|
<div class="flex items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
<span class="text-base-content/70 ml-4">Carregando ausências existentes...</span>
|
|
</div>
|
|
{:else}
|
|
<CalendarioAusencias
|
|
{dataInicio}
|
|
{dataFim}
|
|
{ausenciasExistentes}
|
|
onPeriodoSelecionado={handlePeriodoSelecionado}
|
|
/>
|
|
{/if}
|
|
|
|
{#if dataInicio && dataFim}
|
|
<div class="alert alert-success shadow-lg">
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" />
|
|
<div>
|
|
<h4 class="font-bold">Período selecionado!</h4>
|
|
<p>
|
|
De {parseLocalDate(dataInicio).toLocaleDateString('pt-BR')} até
|
|
{parseLocalDate(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else if passoAtual === 2}
|
|
<!-- Passo 2: Informar Motivo -->
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="mb-2 text-2xl font-bold">Informe o Motivo</h3>
|
|
<p class="text-base-content/70">
|
|
Descreva o motivo da sua solicitação de ausência (mínimo 10 caracteres)
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Resumo do período -->
|
|
{#if dataInicio && dataFim}
|
|
<div class="card border-base-content/20 border-2">
|
|
<div class="card-body">
|
|
<h4 class="card-title text-orange-700 dark:text-orange-400">
|
|
<Calendar class="h-5 w-5" strokeWidth={2} />
|
|
Resumo do Período
|
|
</h4>
|
|
<div class="mt-2 grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p class="text-base-content/70 text-sm">Data Início</p>
|
|
<p class="font-bold">
|
|
{parseLocalDate(dataInicio).toLocaleDateString('pt-BR')}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-base-content/70 text-sm">Data Fim</p>
|
|
<p class="font-bold">
|
|
{parseLocalDate(dataFim).toLocaleDateString('pt-BR')}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-base-content/70 text-sm">Total de Dias</p>
|
|
<p class="text-xl font-bold text-orange-600 dark:text-orange-400">
|
|
{totalDias} dias
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Campo de motivo -->
|
|
<div class="form-control">
|
|
<label class="label" for="motivo">
|
|
<span class="label-text font-bold">Motivo da Ausência</span>
|
|
<span class="label-text-alt">
|
|
{motivo.trim().length}/10 caracteres mínimos
|
|
</span>
|
|
</label>
|
|
<textarea
|
|
id="motivo"
|
|
class="textarea textarea-bordered h-32 text-lg"
|
|
placeholder="Descreva o motivo da sua solicitação de ausência..."
|
|
bind:value={motivo}
|
|
maxlength={500}
|
|
></textarea>
|
|
<label class="label" for="motivo">
|
|
<span class="label-text-alt text-base-content/70">
|
|
Mínimo 10 caracteres. Seja claro e objetivo.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
{#if motivo.trim().length > 0 && motivo.trim().length < 10}
|
|
<div class="alert alert-warning shadow-lg">
|
|
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" />
|
|
<span>O motivo deve ter no mínimo 10 caracteres</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Botões de navegação -->
|
|
<div class="card-actions mt-6 justify-between">
|
|
<button
|
|
type="button"
|
|
class="btn"
|
|
onclick={passoAnterior}
|
|
disabled={passoAtual === 1 || processando}
|
|
>
|
|
<ChevronLeft class="mr-2 h-5 w-5" strokeWidth={2} />
|
|
Voltar
|
|
</button>
|
|
|
|
{#if passoAtual < totalPassos}
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={proximoPasso}
|
|
disabled={processando}
|
|
>
|
|
Próximo
|
|
<ChevronRight class="ml-2 h-5 w-5" strokeWidth={2} />
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn btn-success"
|
|
onclick={enviarSolicitacao}
|
|
disabled={processando || motivo.trim().length < 10}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner"></span>
|
|
Enviando...
|
|
{:else}
|
|
<Check class="mr-2 h-5 w-5" strokeWidth={2} />
|
|
Enviar Solicitação
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Botão cancelar -->
|
|
<div class="mt-4 text-center">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
onclick={() => {
|
|
if (onCancelar) onCancelar();
|
|
}}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal de Erro -->
|
|
<ErrorModal
|
|
open={mostrarModalErro}
|
|
title="Período Indisponível"
|
|
message={mensagemErroModal || 'Já existe uma solicitação para este período.'}
|
|
details={detalhesErroModal}
|
|
onClose={fecharModalErro}
|
|
/>
|
|
|
|
<style>
|
|
.wizard-ausencia {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
}
|
|
</style>
|