Files
sgse-app/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte

720 lines
19 KiB
Svelte

<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import CalendarioFerias from './CalendarioFerias.svelte';
import { toast } from 'svelte-sonner';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
interface Props {
funcionarioId: Id<'funcionarios'>;
onSucesso?: () => void;
onCancelar?: () => void;
}
let { funcionarioId, onSucesso, onCancelar }: Props = $props();
// Cliente Convex
const client = useConvexClient();
// Estado do wizard
let passoAtual = $state(1);
const totalPassos = 3;
// Dados da solicitação
let anoSelecionado = $state(new Date().getFullYear());
let periodosFerias: Array<{
dataInicio: string;
dataFim: string;
dias: number;
}> = $state([]);
let observacao = $state('');
let processando = $state(false);
// Queries
const saldoQuery = $derived(
useQuery(api.saldoFerias.obterSaldo, {
funcionarioId,
anoReferencia: anoSelecionado
})
);
const validacaoQuery = $derived(
periodosFerias.length > 0
? useQuery(api.saldoFerias.validarSolicitacao, {
funcionarioId,
anoReferencia: anoSelecionado,
periodos: periodosFerias.map((p) => ({
dataInicio: p.dataInicio,
dataFim: p.dataFim
}))
})
: { data: null }
);
// Derivados
const saldo = $derived(saldoQuery.data);
const validacao = $derived(validacaoQuery.data);
const totalDiasSelecionados = $derived(periodosFerias.reduce((acc, p) => acc + p.dias, 0));
// Anos disponíveis (últimos 3 anos + próximo ano)
const anosDisponiveis = $derived.by(() => {
const anoAtual = new Date().getFullYear();
return [anoAtual - 1, anoAtual, anoAtual + 1];
});
// Configurações do calendário (baseado no saldo/regime)
const maxPeriodos = $derived(saldo?.regimeTrabalho?.includes('Servidor') ? 2 : 3);
const minDiasPorPeriodo = $derived(saldo?.regimeTrabalho?.includes('Servidor') ? 10 : 5);
// Funções
function proximoPasso() {
if (passoAtual === 1 && !saldo) {
toast.error('Selecione um ano com saldo disponível');
return;
}
if (passoAtual === 2 && periodosFerias.length === 0) {
toast.error('Selecione pelo menos 1 período de férias');
return;
}
if (passoAtual === 2 && validacao && !validacao.valido) {
toast.error('Corrija os erros antes de continuar');
return;
}
if (passoAtual < totalPassos) {
passoAtual++;
}
}
function passoAnterior() {
if (passoAtual > 1) {
passoAtual--;
}
}
async function enviarSolicitacao() {
if (!validacao || !validacao.valido) {
toast.error('Valide os períodos antes de enviar');
return;
}
processando = true;
try {
await client.mutation(api.ferias.criarSolicitacao, {
funcionarioId,
anoReferencia: anoSelecionado,
periodos: periodosFerias.map((p) => ({
dataInicio: p.dataInicio,
dataFim: p.dataFim,
diasCorridos: p.dias
})),
observacao: observacao || undefined
});
toast.success('Solicitação de férias enviada com sucesso! 🎉');
if (onSucesso) onSucesso();
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(errorMessage || 'Erro ao enviar solicitação');
} finally {
processando = false;
}
}
function handlePeriodoAdicionado(periodo: { dataInicio: string; dataFim: string; dias: number }) {
periodosFerias = [...periodosFerias, periodo];
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>
<div class="wizard-ferias-container">
<!-- Progress Bar -->
<div class="mb-8">
<div class="flex items-center justify-between">
<<<<<<< HEAD
{#each Array(totalPassos) as _, i}
=======
{#each Array(totalPassos) as _, i (i)}
>>>>>>> origin
<div class="flex flex-1 items-center">
<!-- Círculo do passo -->
<div
class="relative flex h-12 w-12 items-center justify-center rounded-full font-bold transition-all duration-300"
class:bg-primary={passoAtual > i + 1}
class:text-white={passoAtual > i + 1}
class:border-4={passoAtual === i + 1}
class:border-primary={passoAtual === i + 1}
class:bg-base-200={passoAtual < i + 1}
class:text-base-content={passoAtual < i + 1}
style:box-shadow={passoAtual === i + 1 ? '0 0 20px rgba(102, 126, 234, 0.5)' : 'none'}
>
{#if passoAtual > i + 1}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 13l4 4L19 7"
/>
</svg>
{:else}
{i + 1}
{/if}
</div>
<!-- Linha conectora -->
{#if i < totalPassos - 1}
<div
class="mx-2 h-1 flex-1 transition-all duration-300"
class:bg-primary={passoAtual > i + 1}
class:bg-base-300={passoAtual <= i + 1}
></div>
{/if}
</div>
{/each}
</div>
<!-- Labels dos passos -->
<div class="mt-4 flex justify-between px-1">
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 1}>Ano & Saldo</p>
</div>
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 2}>Períodos</p>
</div>
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 3}>Confirmação</p>
</div>
</div>
</div>
<!-- Conteúdo dos Passos -->
<div class="wizard-content">
<!-- PASSO 1: Ano & Saldo -->
{#if passoAtual === 1}
<div class="passo-content animate-fadeIn">
<h2
class="from-primary to-secondary mb-6 bg-linear-to-r bg-clip-text text-center text-3xl font-bold text-transparent"
>
Escolha o Ano de Referência
</h2>
<!-- Seletor de Ano -->
<div class="mb-8 grid grid-cols-3 gap-4">
<<<<<<< HEAD
{#each anosDisponiveis as ano}
=======
{#each anosDisponiveis as ano (ano)}
>>>>>>> origin
<button
type="button"
class="btn btn-lg transition-all duration-300 hover:scale-105"
class:btn-primary={anoSelecionado === ano}
class:btn-outline={anoSelecionado !== ano}
onclick={() => (anoSelecionado = ano)}
>
{ano}
</button>
{/each}
</div>
<!-- Card de Saldo -->
{#if saldoQuery.isLoading}
<div class="skeleton h-64 w-full rounded-2xl"></div>
{:else if saldo}
<div
class="card from-primary/10 to-secondary/10 border-primary/20 border-2 bg-linear-to-br shadow-2xl"
>
<div class="card-body">
<h3 class="card-title mb-4 text-2xl">
📊 Saldo de Férias {anoSelecionado}
</h3>
<div class="stats stats-vertical lg:stats-horizontal w-full shadow-lg">
<div class="stat">
<div class="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-8 w-8 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div class="stat-title">Total Direito</div>
<div class="stat-value text-primary">{saldo.diasDireito}</div>
<div class="stat-desc">dias no ano</div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-8 w-8 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
</div>
<div class="stat-title">Disponível</div>
<div class="stat-value text-success">
{saldo.diasDisponiveis}
</div>
<div class="stat-desc">para usar</div>
</div>
<div class="stat">
<div class="stat-figure text-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-8 w-8 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<div class="stat-title">Usado</div>
<div class="stat-value text-warning">{saldo.diasUsados}</div>
<div class="stat-desc">até agora</div>
</div>
</div>
<!-- Informações do Regime -->
<div class="alert alert-info mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<h4 class="font-bold">{saldo.regimeTrabalho}</h4>
<p class="text-sm">
Período aquisitivo: {new Date(saldo.dataInicio).toLocaleDateString('pt-BR')}
a {new Date(saldo.dataFim).toLocaleDateString('pt-BR')}
</p>
</div>
</div>
{#if saldo.diasDisponiveis === 0}
<div class="alert alert-warning mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<span>Você não tem saldo disponível para este ano.</span>
</div>
{/if}
</div>
</div>
{:else}
<div class="alert alert-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<span>Nenhum saldo encontrado para este ano.</span>
</div>
{/if}
</div>
{/if}
<!-- PASSO 2: Seleção de Períodos -->
{#if passoAtual === 2}
<div class="passo-content animate-fadeIn">
<h2
class="from-primary to-secondary mb-6 bg-linear-to-r bg-clip-text text-center text-3xl font-bold text-transparent"
>
Selecione os Períodos de Férias
</h2>
<!-- Resumo rápido -->
<div class="alert bg-base-200 mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<p>
<strong>Saldo disponível:</strong>
{saldo?.diasDisponiveis || 0} dias |
<strong>Selecionados:</strong>
{totalDiasSelecionados} dias | <strong>Restante:</strong>
{(saldo?.diasDisponiveis || 0) - totalDiasSelecionados} dias
</p>
</div>
</div>
<!-- Calendário -->
<CalendarioFerias
periodosExistentes={periodosFerias}
onPeriodoAdicionado={handlePeriodoAdicionado}
onPeriodoRemovido={handlePeriodoRemovido}
{maxPeriodos}
{minDiasPorPeriodo}
modoVisualizacao="month"
></CalendarioFerias>
<!-- Validações -->
{#if validacao && periodosFerias.length > 0}
<div class="mt-6">
{#if validacao.valido}
<div class="alert alert-success">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>✅ Períodos válidos! Total: {validacao.totalDias} dias</span>
</div>
{:else}
<div class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<p class="font-bold">Erros encontrados:</p>
<ul class="list-inside list-disc">
<<<<<<< HEAD
{#each validacao.erros as erro}
=======
{#each validacao.erros as erro (erro)}
>>>>>>> origin
<li>{erro}</li>
{/each}
</ul>
</div>
</div>
{/if}
{#if validacao.avisos.length > 0}
<div class="alert alert-warning mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<div>
<p class="font-bold">Avisos:</p>
<ul class="list-inside list-disc">
<<<<<<< HEAD
{#each validacao.avisos as aviso}
=======
{#each validacao.avisos as aviso (aviso)}
>>>>>>> origin
<li>{aviso}</li>
{/each}
</ul>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
<!-- PASSO 3: Confirmação -->
{#if passoAtual === 3}
<div class="passo-content animate-fadeIn">
<h2
class="from-primary to-secondary mb-6 bg-linear-to-r bg-clip-text text-center text-3xl font-bold text-transparent"
>
Confirme sua Solicitação
</h2>
<!-- Resumo Final -->
<div class="card bg-base-100 shadow-2xl">
<div class="card-body">
<h3 class="card-title mb-4 text-xl">📝 Resumo da Solicitação</h3>
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title">Ano de Referência</div>
<div class="stat-value text-primary">{anoSelecionado}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title">Total de Dias</div>
<div class="stat-value text-success">
{totalDiasSelecionados}
</div>
</div>
</div>
<h4 class="mb-2 text-lg font-bold">Períodos Selecionados:</h4>
<div class="space-y-3">
<<<<<<< HEAD
{#each periodosFerias as periodo, index}
=======
{#each periodosFerias as periodo, index (index)}
>>>>>>> origin
<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">
{new Date(periodo.dataInicio).toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
até
{new Date(periodo.dataFim).toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</p>
<p class="text-base-content/70 text-sm">
{periodo.dias} dias corridos
</p>
</div>
</div>
{/each}
</div>
<!-- Campo de Observação -->
<div class="form-control mt-6">
<label for="observacao" class="label">
<span class="label-text font-semibold">Observações (opcional)</span>
</label>
<textarea
id="observacao"
class="textarea textarea-bordered h-24"
placeholder="Adicione alguma observação ou justificativa..."
bind:value={observacao}
></textarea>
</div>
</div>
</div>
</div>
{/if}
</div>
<!-- Botões de Navegação -->
<div class="mt-8 flex justify-between">
<div>
{#if passoAtual > 1}
<button type="button" class="btn btn-outline btn-lg gap-2" onclick={passoAnterior}>
<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="M15 19l-7-7 7-7"
/>
</svg>
Voltar
</button>
{:else if onCancelar}
<<<<<<< HEAD
<button type="button" class="btn btn-lg" onclick={onCancelar}> Cancelar </button>
=======
<button type="button" class="btn btn-ghost btn-lg" onclick={onCancelar}> Cancelar </button>
>>>>>>> origin
{/if}
</div>
<div>
{#if passoAtual < totalPassos}
<button
type="button"
class="btn btn-primary btn-lg gap-2"
onclick={proximoPasso}
disabled={passoAtual === 1 && (!saldo || saldo.diasDisponiveis === 0)}
>
Próximo
<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="M9 5l7 7-7 7"
/>
</svg>
</button>
{:else}
<button
type="button"
class="btn btn-success btn-lg gap-2"
onclick={enviarSolicitacao}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner"></span>
Enviando...
{:else}
<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="M5 13l4 4L19 7"
/>
</svg>
Enviar Solicitação
{/if}
</button>
{/if}
</div>
</div>
</div>
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}
.wizard-ferias-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.passo-content {
min-height: 500px;
}
/* Gradiente no texto */
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
/* Responsive */
@media (max-width: 768px) {
.wizard-ferias-container {
padding: 1rem;
}
.passo-content {
min-height: 400px;
}
}
</style>