305 lines
9.5 KiB
Svelte
305 lines
9.5 KiB
Svelte
<script lang="ts">
|
|
import { useConvexClient } from "convex-svelte";
|
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
|
|
|
interface Periodo {
|
|
id: string;
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
diasCorridos: number;
|
|
}
|
|
|
|
interface Props {
|
|
funcionarioId: string;
|
|
onSucesso?: () => void;
|
|
onCancelar?: () => void;
|
|
}
|
|
|
|
let { funcionarioId, onSucesso, onCancelar }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
|
|
let anoReferencia = $state(new Date().getFullYear());
|
|
let observacao = $state("");
|
|
let periodos = $state<Periodo[]>([]);
|
|
let processando = $state(false);
|
|
let erro = $state("");
|
|
|
|
// Adicionar primeiro período ao carregar
|
|
$effect(() => {
|
|
if (periodos.length === 0) {
|
|
adicionarPeriodo();
|
|
}
|
|
});
|
|
|
|
function adicionarPeriodo() {
|
|
if (periodos.length >= 3) {
|
|
erro = "Máximo de 3 períodos permitidos";
|
|
return;
|
|
}
|
|
|
|
periodos.push({
|
|
id: crypto.randomUUID(),
|
|
dataInicio: "",
|
|
dataFim: "",
|
|
diasCorridos: 0,
|
|
});
|
|
}
|
|
|
|
function removerPeriodo(id: string) {
|
|
periodos = periodos.filter(p => p.id !== id);
|
|
}
|
|
|
|
function calcularDias(periodo: Periodo) {
|
|
if (!periodo.dataInicio || !periodo.dataFim) {
|
|
periodo.diasCorridos = 0;
|
|
return;
|
|
}
|
|
|
|
const inicio = new Date(periodo.dataInicio);
|
|
const fim = new Date(periodo.dataFim);
|
|
|
|
if (fim < inicio) {
|
|
erro = "Data final não pode ser anterior à data inicial";
|
|
periodo.diasCorridos = 0;
|
|
return;
|
|
}
|
|
|
|
const diff = fim.getTime() - inicio.getTime();
|
|
const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
|
|
periodo.diasCorridos = dias;
|
|
erro = "";
|
|
}
|
|
|
|
function validarPeriodos(): boolean {
|
|
if (periodos.length === 0) {
|
|
erro = "Adicione pelo menos 1 período";
|
|
return false;
|
|
}
|
|
|
|
for (const periodo of periodos) {
|
|
if (!periodo.dataInicio || !periodo.dataFim) {
|
|
erro = "Preencha as datas de todos os períodos";
|
|
return false;
|
|
}
|
|
|
|
if (periodo.diasCorridos <= 0) {
|
|
erro = "Todos os períodos devem ter pelo menos 1 dia";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Verificar sobreposição de períodos
|
|
for (let i = 0; i < periodos.length; i++) {
|
|
for (let j = i + 1; j < periodos.length; j++) {
|
|
const p1Inicio = new Date(periodos[i].dataInicio);
|
|
const p1Fim = new Date(periodos[i].dataFim);
|
|
const p2Inicio = new Date(periodos[j].dataInicio);
|
|
const p2Fim = new Date(periodos[j].dataFim);
|
|
|
|
if (
|
|
(p2Inicio >= p1Inicio && p2Inicio <= p1Fim) ||
|
|
(p2Fim >= p1Inicio && p2Fim <= p1Fim) ||
|
|
(p1Inicio >= p2Inicio && p1Inicio <= p2Fim)
|
|
) {
|
|
erro = "Os períodos não podem se sobrepor";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function enviarSolicitacao() {
|
|
if (!validarPeriodos()) return;
|
|
|
|
try {
|
|
processando = true;
|
|
erro = "";
|
|
|
|
await client.mutation(api.ferias.criarSolicitacao, {
|
|
funcionarioId: funcionarioId as any,
|
|
anoReferencia,
|
|
periodos: periodos.map(p => ({
|
|
dataInicio: p.dataInicio,
|
|
dataFim: p.dataFim,
|
|
diasCorridos: p.diasCorridos,
|
|
})),
|
|
observacao: observacao || undefined,
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e: any) {
|
|
erro = e.message || "Erro ao enviar solicitação";
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
periodos.forEach(p => calcularDias(p));
|
|
});
|
|
</script>
|
|
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl mb-4">
|
|
<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="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
Solicitar Férias
|
|
</h2>
|
|
|
|
<!-- Ano de Referência -->
|
|
<div class="form-control">
|
|
<label class="label" for="ano-referencia">
|
|
<span class="label-text font-semibold">Ano de Referência</span>
|
|
</label>
|
|
<input
|
|
id="ano-referencia"
|
|
type="number"
|
|
class="input input-bordered w-full max-w-xs"
|
|
bind:value={anoReferencia}
|
|
min={new Date().getFullYear()}
|
|
max={new Date().getFullYear() + 2}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Períodos -->
|
|
<div class="mt-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="font-semibold text-lg">Períodos ({periodos.length}/3)</h3>
|
|
{#if periodos.length < 3}
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-primary gap-2"
|
|
onclick={adicionarPeriodo}
|
|
>
|
|
<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="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Adicionar Período
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
{#each periodos as periodo, index}
|
|
<div class="card bg-base-200 border border-base-300">
|
|
<div class="card-body p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h4 class="font-medium">Período {index + 1}</h4>
|
|
{#if periodos.length > 1}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-error btn-square"
|
|
aria-label="Remover período"
|
|
onclick={() => removerPeriodo(periodo.id)}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" 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>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="form-control">
|
|
<label class="label" for={`inicio-${periodo.id}`}>
|
|
<span class="label-text">Data Início</span>
|
|
</label>
|
|
<input
|
|
id={`inicio-${periodo.id}`}
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={periodo.dataInicio}
|
|
onchange={() => calcularDias(periodo)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for={`fim-${periodo.id}`}>
|
|
<span class="label-text">Data Fim</span>
|
|
</label>
|
|
<input
|
|
id={`fim-${periodo.id}`}
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={periodo.dataFim}
|
|
onchange={() => calcularDias(periodo)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for={`dias-${periodo.id}`}>
|
|
<span class="label-text">Dias Corridos</span>
|
|
</label>
|
|
<div id={`dias-${periodo.id}`} class="flex items-center h-9 px-3 bg-base-300 rounded-lg" role="textbox" aria-readonly="true">
|
|
<span class="font-bold text-lg">{periodo.diasCorridos}</span>
|
|
<span class="ml-1 text-sm">dias</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Observações -->
|
|
<div class="form-control mt-6">
|
|
<label class="label" for="observacao">
|
|
<span class="label-text font-semibold">Observações (opcional)</span>
|
|
</label>
|
|
<textarea
|
|
id="observacao"
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Adicione observações sobre sua solicitação..."
|
|
bind:value={observacao}
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Erro -->
|
|
{#if erro}
|
|
<div class="alert alert-error mt-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" 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>
|
|
<span>{erro}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Ações -->
|
|
<div class="card-actions justify-end mt-6">
|
|
{#if onCancelar}
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
onclick={onCancelar}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary gap-2"
|
|
onclick={enviarSolicitacao}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|