379 lines
13 KiB
Svelte
379 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { useConvexClient } from "convex-svelte";
|
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
|
|
|
interface Periodo {
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
diasCorridos: number;
|
|
}
|
|
|
|
interface Props {
|
|
solicitacao: any;
|
|
gestorId: string;
|
|
onSucesso?: () => void;
|
|
onCancelar?: () => void;
|
|
}
|
|
|
|
let { solicitacao, gestorId, onSucesso, onCancelar }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
|
|
let modoAjuste = $state(false);
|
|
let periodos = $state<Periodo[]>([]);
|
|
let motivoReprovacao = $state("");
|
|
let processando = $state(false);
|
|
let erro = $state("");
|
|
|
|
$effect(() => {
|
|
if (modoAjuste && periodos.length === 0) {
|
|
periodos = solicitacao.periodos.map((p: any) => ({...p}));
|
|
}
|
|
});
|
|
|
|
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 = "";
|
|
}
|
|
|
|
async function aprovar() {
|
|
try {
|
|
processando = true;
|
|
erro = "";
|
|
|
|
await client.mutation(api.ferias.aprovar, {
|
|
solicitacaoId: solicitacao._id,
|
|
gestorId: gestorId as any,
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e: any) {
|
|
erro = e.message || "Erro ao aprovar solicitação";
|
|
} 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, {
|
|
solicitacaoId: solicitacao._id,
|
|
gestorId: gestorId as any,
|
|
motivoReprovacao,
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e: any) {
|
|
erro = e.message || "Erro ao reprovar solicitação";
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function ajustarEAprovar() {
|
|
try {
|
|
processando = true;
|
|
erro = "";
|
|
|
|
await client.mutation(api.ferias.ajustarEAprovar, {
|
|
solicitacaoId: solicitacao._id,
|
|
gestorId: gestorId as any,
|
|
novosPeriodos: periodos,
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e: any) {
|
|
erro = e.message || "Erro ao ajustar e aprovar solicitação";
|
|
} 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",
|
|
};
|
|
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",
|
|
};
|
|
return textos[status] || status;
|
|
}
|
|
|
|
function formatarData(data: number) {
|
|
return new Date(data).toLocaleString("pt-BR");
|
|
}
|
|
</script>
|
|
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h2 class="card-title text-2xl">
|
|
{solicitacao.funcionario?.nome || "Funcionário"}
|
|
</h2>
|
|
<p class="text-sm text-base-content/70 mt-1">
|
|
Ano de Referência: {solicitacao.anoReferencia}
|
|
</p>
|
|
</div>
|
|
<div class={`badge ${getStatusBadge(solicitacao.status)} badge-lg`}>
|
|
{getStatusTexto(solicitacao.status)}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Períodos Solicitados -->
|
|
<div class="mt-4">
|
|
<h3 class="font-semibold text-lg mb-3">Períodos Solicitados</h3>
|
|
<div class="space-y-2">
|
|
{#each solicitacao.periodos as periodo, index}
|
|
<div class="flex items-center gap-4 p-3 bg-base-200 rounded-lg">
|
|
<div class="badge badge-primary">{index + 1}</div>
|
|
<div class="flex-1 grid grid-cols-3 gap-2 text-sm">
|
|
<div>
|
|
<span class="text-base-content/70">Início:</span>
|
|
<span class="font-semibold ml-1">{new Date(periodo.dataInicio).toLocaleDateString("pt-BR")}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-base-content/70">Fim:</span>
|
|
<span class="font-semibold ml-1">{new Date(periodo.dataFim).toLocaleDateString("pt-BR")}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-base-content/70">Dias:</span>
|
|
<span class="font-bold ml-1 text-primary">{periodo.diasCorridos}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Observações -->
|
|
{#if solicitacao.observacao}
|
|
<div class="mt-4">
|
|
<h3 class="font-semibold mb-2">Observações</h3>
|
|
<div class="p-3 bg-base-200 rounded-lg text-sm">
|
|
{solicitacao.observacao}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Histórico -->
|
|
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0}
|
|
<div class="mt-4">
|
|
<h3 class="font-semibold mb-2">Histórico</h3>
|
|
<div class="space-y-1">
|
|
{#each solicitacao.historicoAlteracoes as hist}
|
|
<div class="text-xs text-base-content/70 flex items-center gap-2">
|
|
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>{formatarData(hist.data)}</span>
|
|
<span>-</span>
|
|
<span>{hist.acao}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Ações (apenas para status aguardando_aprovacao) -->
|
|
{#if solicitacao.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}
|
|
>
|
|
<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>
|
|
Aprovar
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-info gap-2"
|
|
onclick={() => modoAjuste = true}
|
|
disabled={processando}
|
|
>
|
|
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
Ajustar Datas e Aprovar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Reprovar -->
|
|
<div class="card bg-base-200">
|
|
<div class="card-body p-4">
|
|
<h4 class="font-semibold text-sm mb-2">Reprovar Solicitação</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()}
|
|
>
|
|
<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>
|
|
Reprovar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- Modo Ajuste -->
|
|
<div class="space-y-4">
|
|
<h4 class="font-semibold">Ajustar Períodos</h4>
|
|
{#each periodos as periodo, index}
|
|
<div class="card bg-base-200">
|
|
<div class="card-body p-4">
|
|
<h5 class="font-medium mb-2">Período {index + 1}</h5>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div class="form-control">
|
|
<label class="label" for={`ajuste-inicio-${index}`}>
|
|
<span class="label-text text-xs">Início</span>
|
|
</label>
|
|
<input
|
|
id={`ajuste-inicio-${index}`}
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={periodo.dataInicio}
|
|
onchange={() => calcularDias(periodo)}
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for={`ajuste-fim-${index}`}>
|
|
<span class="label-text text-xs">Fim</span>
|
|
</label>
|
|
<input
|
|
id={`ajuste-fim-${index}`}
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
bind:value={periodo.dataFim}
|
|
onchange={() => calcularDias(periodo)}
|
|
/>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for={`ajuste-dias-${index}`}>
|
|
<span class="label-text text-xs">Dias</span>
|
|
</label>
|
|
<div id={`ajuste-dias-${index}`} class="flex items-center h-9 px-3 bg-base-300 rounded-lg" role="textbox" aria-readonly="true">
|
|
<span class="font-bold">{periodo.diasCorridos}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
|
|
<div class="flex gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost 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}
|
|
>
|
|
<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="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Confirmar e Aprovar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- Motivo Reprovação (se reprovado) -->
|
|
{#if solicitacao.status === "reprovado" && solicitacao.motivoReprovacao}
|
|
<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>
|
|
<div>
|
|
<div class="font-bold">Motivo da Reprovação:</div>
|
|
<div class="text-sm">{solicitacao.motivoReprovacao}</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- 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}
|
|
|
|
<!-- Botão Fechar -->
|
|
{#if onCancelar}
|
|
<div class="card-actions justify-end mt-4">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
onclick={onCancelar}
|
|
disabled={processando}
|
|
>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|